- 投稿日:2020-04-21T23:40:22+09:00
インクリメンタルサーチを噛み砕く
近況報告
やあ。テックキャンプのリモートワーク化がはじまって四週間目に突入しました。我がチームはついに最終課題をパスし間も無く就職活動に入ります。そこで大きな壁,履歴書の自己開示。わたしが一番苦手とする場所です。「わたしはプログラミングでご飯食べていくってきめたの!」って叫んで内定決まればいいのに笑
さて,今日の記事は個人アプリにインクリメンタルサーチを実装のついでの忘備録です。実況感覚で書いていきます。
インクリメンタルサーチ
なにそれ
英語でincremental search,直訳は増加検索ですね。通常の検索は検索ボタンを押すとページが更新されて検索結果が出てきますが,この検索は一文字入れるごとに非同期通信で検索結果がぽんぽん出てきます。youtubeやGoogleとか検索の予測がでますね。あれです。
文字を打つごとに予測される結果が減るからデクリメンタルサーチでもいいんじゃね。ちなみに,予測の変換に履歴が出てくるのはcookieがかかわるので少々異なります。非同期通信・・・ユーザーの望むタイミングとは別で行われる通信です。通信自体は行われています 。railsなら,ユーザーのリクエストをMVCで逐一処理してそれに応じたビューをユーザーに届けています。ただ,例えばgoogle検索で「ツイッ」まで書いたところでリダイレクトして予測変換「ツイッター」が記載されたビューに更新されたとします。検索フォームの部分だけ変わればいいのに,変化のないGoogleのロゴとかも更新されるの無駄な気がしません?痒いところに届くのが非同期通信です。私のイメージは,必要な情報だけをビューに上書きする通信です。
なんで必要?
いちいち検索ボタンを押して読み込むより早く検索結果をユーザーに届けることができます。個人アプリのような小さい規模ではそこまで破壊力があるわけではありません。ただ,ロゴが舌なめずりしている某サイトだと通信が0.1秒遅れると売り上げが1%落ちると聞きました。1兆円の売り上げなら100億円の損失です。まさに時は金なりですね!
環境(前提)
・rails 2.5.2
・ruby 5.2.3
gem 'jquery-rails'
DBに検索対象のレコードが入っている
検索機能の実装完了前提今回はjqueryのgemを用いるから
gem 'jquery-rails'からのbundle install。
application.jsでjqueryが使えるように,//= require jqueryの記述の有無を確認。
これで準備おっけー
実装(実況感覚)
検索フォームに入力するとテーブルのnameカラムを検索する機能を実装していきます。
インデックスの実装
インデックスってなんだっけ?笑
カリキュラムを見直すと,検索の際にどれを検索すればいいのかをピックアップしてくれる機能なんだってさ。
UFOキャッチャーで例えると,UFOが検索するための端末としたらインデックスは商品(レコード)が引っかかりやすくなるために付いているリングみたいな役割なのかな。ワカリニクイタトエデースただ,インデックスは検索に用いるカラムのみを複製して1つのテーブルみたいにしてるから,作りすぎるとデータベースの容量の圧迫になるみたい。
さて,インデックスを追加していきます
$ rails g migration AddIndexTo検索したいテーブルs実行したら,マイグレーションファイルに飛んで
class AddIndexTo検索したいテーブル名 < ActiveRecord::Migration def change add_index :検索したいテーブルs, :検索したいカラム, length: 32(最大文字数) end endを記述したらマイグレート。ミスったらDB:rollbackなれdb:dropとなれ。
APIの準備
インクリメンタルサーチの実装の順番はrailsの実装とあんまり変わりません。
コントローラーに条件分岐の記入reviews_controller.rbdef search @reviews = Reviews.search(params[:keyword]) respond_to do |format| format.html format.json endformatごとのレスポンスで場合分けします。
format.htmlはhtml全部読み込みますよ,format.jsonはJS形式のみ読み込みますよって感じ。この分岐のおかげで非同期通信が可能になります。コントローラーで条件分岐したら,DBから該当のでーたをビューまで運ぶ箱,jbuilder(パラムスに似てるね)を作成する。検索しているテーブルのビューファイルにフォルダsearch.json.jbuilderを作成。jbuilderを用いると,そのままJSでも用いることができる。
search.json.jbuilderjson.array! @reviews do |review| ←検索に引っかかった各インスタンスを配列でバラすぜ!中身は以下の通りだ! json.id review.id json.content review.content json.image review.image json.taste review.taste json.fragrance review.fragrance json.individuality review.individuality json.fruity review.fruity json.smorky review.smorky json.age review.age json.user_id review.user_id json.user_sign_in current_user endテキストフィールドの実装
次はビューの作成をしていきます。検索フォームはこんな感じ↓
index.html.erb<%= form_with(url: review_tweets_path, local: true, method: :get, class: "search-form") do |form| %> <%= form.text_field :keyword, placeholder: "検索する", class: "search-input" %> <%= form.submit "検索", class: "search-btn" %> <% end %>ではJSの記述に入ります。記述量が多いので少しずつ刻んでいきます。
search.js$(function() { $(".search-input").on("keyup", function() { ←フォームに何か打ち込まれたら作動するよ var input = $(".search-input").val(); ←inputを打ちこまれた文字と定義するよ console.log(input); ←JSが動いているか確認。うまく表示されていたら消す。 }); });フォームの情報をJSON形式でコントローラーに送る
今までやったことの確認をすると,
・respond_toで行き先決定
・jbuilderでデータベースの情報の引き出し準備
・ビューでフォームの情報をJSで抽出する
うん,MVCだね。railsは非同期通信でも大まかなやることは同じです。もしこれ行こうエラーが発生したらMVCに則ってエラーを解消していきます。search.js$(function() { $(".search-input").on("keyup", function() { var input = $(".search-input").val(); //以下を追加 $.ajax({ ←ajaxは非同期通信するよ!っていう合図 type: 'GET', ←HTTPメソッドはDBを得るからGET url: '/review/search', ←非同期通信が行われるurl data: { keyword: input }, ←さーばーに送信するキー。これを元にインデックスから引っ張ってくる。 dataType: 'json'←データの形式 }) }); });これでフォームの情報はJSON形式で送られ,コントローラーのrespond_toでJSONに振り分けられてモデルに進みます。多分。
非同期通信がビューまで届いているか確認します。メソッドdoneを用いて届いた情報をみてみる。
search.js$(function() { $(".search-input").on("keyup", function() { var input = $(".search-input").val(); $.ajax({ type: 'GET', url: '/review/search', data: { keyword: input }, dataType: 'json' }) .done(function(reviews) { console.log(reviews); }) }); });事故発生。読み込めてない。。。順番に確認していこう。
JSは読み込めていたから,クラスのミスはない。
ajaxの読み込みミスは、、、url間違えていました、、、
分岐は誤字なし,jbuilderは、、、nameを書き忘れる痛恨のミス!何調べようとしてたんだも一回検索フォームにキーワードを打ち込む、、、うん、うまくいった!!!
さて,次に進みます。次はJSを利用してHTMLを上書きします。
まずdone以下を以下のように編集します。search.js~省略~ .done(function(reviews) { ←reviewを引張ってきたから以下の実行するよ $(".showZone").empty(); ←該当クラスの中の子要素全て消すね if (reviews.length !== 0) { ←もし1でもあったら reviews.forEach(function(review){ ←1つずつ並べて appendReview(review); ←そのレコードを出現させるよ }); } else { appendErrMsgToHTML("一致するツイートがありません"); } }) ~省略~次はappendReview(review)に該当する定義を作成してクラスshowZoneに上書きするHTMLを作成します。
search.js$(function() { var search_list = $(".showZone"); ←こういうやつは先に定義にしとく function appendReview(review) { var html = `<div class="reviewWhisky"> <a href="/reviews/${review.id}"> <div class="topZone"> <div class="topZone__image"> <img id="goShowPage" src="${review.image.url}" width="180" height="180"> </div> <div class="bottomZone"> <p>${review.name}</p> </div></div></a></div>` //htmlは検証からコピーが最速。データベースからの読み込み表示は上の書き方参考にしてね search_list.append(html); } function appendErrMsgToHTML(msg) { var html = `<div class='name'>${ msg }</div>` search_list.append(html); $(".search-input").on("keyup", function() { var input = $(".search-input").val(); $.ajax({ type: 'GET', url: '/reviews/search', data: { keyword: input }, dataType: 'json' }) .done(function(reviews) { search_list.empty(); if (reviews.length !== 0) { reviews.forEach(function(review){ appendReview(review); ←appendはHTML召喚のメソッドと覚えておけばOK }); } else { appendErrMsgToHTML("一致するのがありません"); } }) }); });では,もし何も投稿が該当しなかった時用のを用意しましょ。いま一枚もない場合,何も写っていない状態だから,バグじゃないよーてわかるような記述を書きますappendErrMsgToHTMLに意味を持たせます)。
search.js~省略~ search_list.append(html); } function appendErrMsgToHTML(msg) { var html = `<div class='name'>${ msg }</div>`←新しい要素なので必要ならCSSを search_list.append(html); ~省略~ .fail(function() {←うまく行かなかった時用にエラー表示を準備 alert('error'); }); }); });これで一通り完成かな。ちなみに中の人はキータ執筆と同時進行で行なっていたのですが,search.jsファイルが読み込まれなくなる不具合に遭遇し2時間詰みました笑
解決策としては別のファイルを作成して作り直したらうまくいきまっした、、よかった、、、ただ,何が原因のエラーだったんだろう。。。
おわりに
まあなんとか実装できました。非同期通信のjsはコード1つ1つがわかりやすいので比較的サクッといけますね。非同期通信で重要なのは,通信の根本はMVCであること!
余談
非同期通信とturbolinks(以下TB)は仲が悪いみたいです。TBってなんやねんって話ですよね。カリキュラムでも無下に扱われていました。この子は名前の通り,リンク先に早くたどり着くため手助けをしてくれます。ただ,早くする方法というのは,余計な動きを削ること,ここではJSを起動しないことです。非同期通信はJSを用いた通信なので,turbolinksが邪魔をしてしまう可能性がある(実際邪魔する)訳ですねー。
ただ悪い子ではないので見つけ次第gemをコメントアウト!とかしないであげてください。
$(document).on('turbolinks:load', function(){
とJSの初めに打ってあげたり,link_toにつづけて
data:{"turbolinks": :false}
と打ってあげると,JSが動いてくれます
- 投稿日:2020-04-21T23:26:13+09:00
【Rails Docker Git】よく使うコマンド一覧
【Rails Docker Git】よく使うコマンド一覧
Dockerでサーバー立ち上げ
terminal> docker-compose upDockerでサーバーダウン
terminal> docker-compose downDockerでDB確認
terminal> docker-compose psrails db:create
terminal> docker-compose exec イメージ名 ./bin/rails db:createrailsタスク一覧(コマンド一覧)
terminal> docker-compose exec web ./bin/rails -Tgitのカレントブランチを確認
terminal> git branchgitのブランチを新規作成してテェックアウト
terminal> git checkout -b ブランチ名gitのステージングされテイルファイルのキャシュをみる
terminal> git diff -cashedterminal> docker-compose exec イメージ名 ./bin/rails db:create
- 投稿日:2020-04-21T23:24:39+09:00
【Rails Tutorial12章】パスワード再設定機能の実装手順を整理してみました【後編】
はじめに
Railsチュートリアル12章の内容を、少しでも理解の助けとなればと思い、割としっかり目に整理しました!
備忘録です。
前提
Railsチュートリアル1〜11章までの内容が完了していること。
特に、アカウント有効化機能が実装されている事。
内容
Railsチュートリアル12章のパスワード再設定機能の実装手順を、前中後半の3回に分けて整理おります。
後編である今回は、パスワード再設定機能の本実装を行っていきます!前編→パスワード再設定用のリソース作成
中編→パスワード再設定メール送信機能の実装
3.パスワードを再設定する。
●送信用メールの作成が完了したので、次はPasswordResetsコントローラのeditアクションの実装を行っていく。
●その次に統合テストを使って上手く動作しているかを確認するためのテストを書いていく。3-1.パスワード再設定機能実装のための準備を行っていく。
1.まずはパスワード再設定用メールに添付してあるリンクを動作させるために、パスワード再設定フォームのビューを作成する必要がある。
➡︎app/views/password_resets/edit.html.erbファイルで行う。➡︎フォームに関してはパスワード入力フィールドと、パスワード確認入力フィールドを作成すれば十分。
2.パスワードを再設定を行おうとしているユーザーを取得する必要がある。
➡︎ユーザーがメールに貼り付けられているリンクをクリックしてパスワード再設定ページへ移動してくる時、そしてパスワードを実際に再設定する時にユーザーを取得する必要がある。➡︎メールアドレスをキーとして取得するのだが、そのためにはeditアクションとupdateアクションの両方で、メールアドレスが必要となる。
→editアクションではリンクに含まれているメアドから検索してそのまま取得できる。
→updateアクションでは、フォームを送信するとメールアドレスの情報は消えてしまうので、上手くユーザーを取得できるように準備をしなければならない。➡︎updateアクションでメールアドレスからユーザーを取得するための準備。
→どうすれば良いかというと、リンクに含まれているメアドを一時的に保持してあげればいい。そのために隠しフィールドを用意してページ内に一旦保持させる手法をとることにする。
→隠しフィールドを用意することで、パスワード再設定の情報と一緒にメールアドレスも送信されるようになる。
→パスワード再設定用のフォームにhidden_field_tagメソッドを使う。キーに:email属性、値に@user.emailでユーザーのアドレスがparams[:email]の値として送信されるようにする。
→<%= hidden_field_tag :email, @user.email %>
→注意。f.hidden_field_tagとしないこと。こうしてしまうと、form_for(@user)のブロック引数を受け取って、params[:user][:email]にメールアドレスが保存されてしまう。
→しかし、これにてメールアドレスからユーザーを検索して取得する準備が整いました。3.次に上記のフォームを描画するための処理を、PasswordResetsコントローラのeditアクションへ記述してあげる。
➡︎app/controllers/password_resets_controller/rbファイルへ移動する。
→まずprivateキーワード内に、メールアドレスと一致するユーザーの検索を行うメソッドを定義する。
→メソッド名をget_userとする。
→params[:email]のメールアドレスを持つユーザーを取得して、@userインスタンス変数へ代入する処理を記述する。
→同じくprivateキーワード内で、取得したユーザーが有効なユーザーかどうかを確認するメソッドを定義する。
→メソッド名をvalid_userとする。
→そのユーザーが存在する事、有効化されている事、認証済みである事をunless文により判定する条件式を記述する。
→@user(ユーザーの存在) && @user.activated?(有効化の確認) && @user.authenticated?(:reset, params[:id])(ダイジェストとトークンの一致を確認)とする。
→正しいユーザーでなければ、ルートURLへダイレクトさせる処理を記述。
→有効なユーザーかどうかの確認は、editアクションで必要なのはもちろんupdateアクションでも必要になるので、beforeフィルターを使ってバリデーションを行ってあげる。➡︎以上でパスワード再設定用のフォームが描画されるようになったので、あとはupdateアクションにパスワードを実際に更新するための処理を記述していく。
app/views/password_resets/edit.html.erb#3-1.2 <% provide(:title, 'パスワードの再設定') %> <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %> <%= render 'shared/error_messages' %> <%= hidden_field_tag :email, @user.email %> <%= f.label :password %> <%= f.password_field :password %> <%= f.label :password_confirmation %> <%= f.password_field :password_confirmation %> <%= f.submit "パスワードを更新する" %> <% end %>app/controllers/password_resets_controller.rb#3-1.3 class PasswordResetsController < ApplicationController before_action :get_user, only: [:edit, :update] before_action :valid_user, only: [:edit, :update] . . . private #メールアドレスと一致するユーザーの検索を行う def get_user @user = User.find_by(email: params[:email]) end #取得したユーザーが有効なユーザーかどうかを確認する def valid_user unless (@user && @user.activated? && @user.authenticated?(:reset, params[:id])) redirect_to root_url end end end3-2.実際にパスワードを更新する機能を実装していく。
➡︎上記で作成したフォームからの送信に対応するupdateアクションを作成していく。
➡︎updateアクションの記述を行っていく上で、4つ考慮しなければならない事項がある。
1つ目:パスワード再設定の有効期限が切れていないかどうかを確認する。
➡︎パスワード再設定の期限を設定して、その期限の間に再設定されなかった場合は期限切れとする処理を行うメソッドを定義する。
→app/models/user.rbファイルで記述する。
→メソッド名をpassword_reset_expired?とすることにする。
→期限は2時間とし、現在時刻から2時間以上経っている場合にはtrueを返す処理を記述する。
→reset_sent_at < 2.hours.ago
→注意。「2時間より少ない」という意味ではなく、「2時間以上前」と読むのが正しい。
→つまり、「パスワード再設定メールの送信時刻が、現在時刻よりも2時間以上早い場合」と読むのが正しい。
→準備が完了したので、パスワード再設定の有効期限が切れているかどうかをチェックするメソッドを定義していく。➡︎パスワード再設定の有効期限が切れていた場合にユーザーを弾くメソッドを定義する。
→app/controllers/password_reset_controller/rbファイルへ移動する。
→メソッド名をcheck_expirationとする。
→取得したユーザーのパスワード再設定有効期限が切れてるかどうかを判定する条件式を記述する。
→上記で準備した「パスワード再設定のメール送信時刻が、現在時刻よりも2時間以上前ならtrueを返す」メソッドを利用する。
→trueならフラッシュで警告メッセージを表示させ、パスワード再設定フォームへリダイレクトさせる。
→このメソッドはprivateキーワード内で定義してあげる。➡︎edit、updateアクションに対しbeforeフィルターで、check_expirationメソッドを適用してあげる。
2つ目:無効なパスワードであれば失敗させる。そして、失敗した理由も表示させる。
➡︎app/controllers/password_resets_controller.rbファイルのupdateアクションに直接記述していく。
→updateアクション内のどの条件式にも当てはまらない場合はeditのビューを再描画させ、パーシャルにエラーメッセージが表示されるようにする(パスワード再設定フォームに記述済み)。3つ目:新しいパスワードが空文字になっていないかを確認する。
➡︎app/controllers/password_resets_controller.rbファイルのupdateアクションに直接記述していく。
→フォームから送信されたパスワードが空かどうかを判定する条件式を記述する。
→空ならerrors.addを使って@userオブジェクトを直接指定して、空の文字列に対してデフォルトのメッセージを表示させるようにする。
→@user.errors.add(:password, :blank)
→空でなければ4の処理へ移行させる。4つ目:新しいパスワードが正しければ、更新する。
➡︎app/controllers/password_resets_controller.rbファイルのupdateアクションに直接記述していく。
→パスワードが更新・保存された場合の条件式を記述する。
→ユーザーをログインさせる。
→ユーザーのリセットダイジェストを、nilで更新・保存する。
→再設定が成功しても最低2時間は再設定が有効になってしまっているので、ダイジェストを空にすることで強制的に無効にさせる。
→フラッシュメッセージを表示させる。
→ユーザー詳細ページへリダイレクトさせる処理を記述する。➡︎上記の条件式ではupdate_attributesメソッドを使用しているが、引数にuser_paramsメソッドを渡すことでより厳密な属性検索をかけさせるようにしたい。
→なのでprivateキーワード内で、パスワードとパスワード確認のみを許可するためのuser_paramsメソッドを定義してあげる。
→params.require(:user).permit(:password, :password_confirmation)➡︎これで、updateアクションが動作するようになった。
app/controllers/password_resets_controller.rbclass PasswordResetsController < ApplicationController . . #1つ目:パスワード再設定の有効期限が切れていないかどうかを確認する。 before_action :check_expiration, only: [:edit, :update] . . . def update #3つ目:新しいパスワードが空文字になっていないかを確認する。 if params[:user][:password].empty? @user.errors.add(:password, :blank) render 'edit' #4つ目:新しいパスワードが正しければ、更新する。 elsif @user.update_attributes(user_params) log_in @user flash[:success] = "パスワードが更新されました" redirect_to @user #2つ目:無効なパスワードであれば失敗させる。 else render 'edit' end end private def user_params params.require(:user).permit(:password, :password_confirmation) end . . . #1つ目:パスワード再設定の有効期限が切れていないかどうかを確認する。 def check_expiration if @user.password_reset_expired? flash[:danger] = "パスワード再設定の有効期限が切れてます" redirect_to new_password_reset_url end end endapp/models/user.rbclass User < ApplicationRecord . . . #1つ目:パスワード再設定の有効期限が切れていないかどうかを確認する。 def password_reset_expired? reset_sent_at < 2.hours.ago end . . . end3-3.パスワードの再設定をテストする。
1.新しいパスワードの送信に成功した場合と、失敗した場合の統合テストを書いていく。
➡︎まずはファイルを作成する。
→$ rails g integration_test password_resets➡︎test/integration/password_resets_test.rbファイルへ移動して、パスワード再設定の統合テストを記述していく。
→(1)setupメソッドを定義する。
→メイラーの中身が空の配列を作成する。
→テスト用のログインユーザーを取得する。
→(2)パスワード再設定のテストを行う。
→パスワード再設定用メール送信フォームに対してGETリクエストを送信させる記述を行う。
→パスワード再設定用メール送信フォームのビューテンプレートが描画されていることを検証。
→(3)まずは無効なメールアドレスで、パスワード再設定用のメール送信をPasswordResetsリソースのcreateアクションへPOSTリクエストを送信させる記述を行う。
→フラッシュが存在することを検証。
→パスワード再設定用のメールを送信するビューテンプレートが表示されていることを検証。
→(4)今度は有効なメールアドレスで、パスワード再設定用のメール送信をPasswordResetsリソースのcreateアクションへPOSTリクエストを送信させる記述を行う。
→更新後のリセットダイジェストが、更新前のダイジェストと(ダイジェストが空になったことで)イコールでないことを検証する。
→メイラーの配列の値の個数が1になっていることを検証する。
→フラッシュが表示されていることを検証する。
→ルートURLへリダイレクトされていることを検証する。
→(5)パスワード再設定フォームのテストを行う。
→assignsメソッドでPasswordResetsコントローラから直接@userインスタンス変数を参照し、ユーザーを取得しておく。
→(6)無効なメールアドレスで、パスワード再設定フォームへGETリクエストを送信させる記述を行う。
→ルートURLへリダイレクトされることを検証する。
→(7)ここでユーザーを無効なユーザーに切り替える処理を記述(攻撃者によるアカウント乗っ取りのテスト)。
→メールアドレスは有効だが、無効なユーザーでパスワード再設定フォームヘGETリクエストを送信させる記述を行う。
→ルートURLへリダイレクトされることを検証する。
→(8)そして今度は有効なユーザーへ切り替える。
→メールアドレスは有効だがトークンが無効なまま、パスワード再設定フォームヘGETリクエストを送信させる記述を行う。
→ルートURLへリダイレクトされることを検証する。
→(9)有効なメールアドレスとトークンで、パスワード再設定フォームヘGETリクエストを送信させる記述を行う。
→ちゃんとパスワード再設定ビューテンプレートが描画されていることを検証する。
→メールアドレス用の隠しフィールドセレクタのvalue値に、取得したユーザーのメールアドレスが表示されていることを検証する。
→(10)無効なパスワードとパスワード確認を、PasswordResetsコントローラのupdateアクションヘPATCHリクエストを送信してしまった場合の記述をする。
→エラーメッセージセレクタが表示されていることを検証する。
→(11)パスワードとパスワード確認入力が空のまま、PasswordResetsコントローラのupdateアクションヘPATCHリクエストを送信してしまった場合の記述をする。
→この場合もエラーメッセージセレクタが表示されていることを検証する。
→(12)有効なパスワードとパスワード確認入力で、PasswordResetsコントローラのupdateアクションヘPATCHリクエストが送信された場合の記述をする。
→ログインユーザーが存在することを検証する。
→フラッシュが表示されていることを確認する。
→そのユーザーのページへ、ちゃんとリダイレクトされているかどうかを検証する。➡︎テストスイートGREEN。
test/integration/password_resets_test.rbrequire 'test_helper' class PasswordResetsTest < ActionDispatch::IntegrationTest #(1)setupメソッドを定義する。 def setup ActionMailer::Base.deliveries.clear @user = users(:テスト用のユーザー) end #(2)パスワード再設定のテストを行う。 test "password resets" do get new_password_reset_path assert_template 'password_resets/new' #(3)まずは無効なメールアドレスで、パスワード再設定用のメール送信をPasswordResetsリソースのcreateアクションへPOSTリクエストを送信させる記述を行う post password_resets_path, params: { password_reset: { email: "無効なメアド" } } assert_not flash.empty? assert_template 'password_resets/new' #(4)今度は有効なメールアドレスで、パスワード再設定用のメール送信をPasswordResetsリソースのcreateアクションへPOSTリクエストを送信させる記述を行う。 post password_resets_path, params: { password_reset: { email: @user.email } } assert_not_equal @user.reset_digest, @user.reload.reset_digest assert_equal 1, ActionMailer::Base.deliveries.size assert_not flash.empty? assert_redirected_to root_url #(5)パスワード再設定フォームのテストを行う。 user = assigns(:user) #(6)無効なメールアドレスで、パスワード再設定フォームへGETリクエストを送信させる記述を行う。 get edit_password_reset_path(user.reset_token, email: "無効なメアド") assert_redirected_to root_url #(7)ここでユーザーを無効なユーザーに切り替える処理を記述(攻撃者によるアカウント乗っ取りのテスト)。 user.toggle!(:activated) get edit_password_reset_path(user.reset_token, email: user.email) assert_redirected_to root_url #(8)そして今度は有効なユーザーで(トークンは無効)。 get edit_password_reset_path('無効なトークン', email: user.email) assert_redirected_to root_url #(9)有効なメールアドレスとトークンで、パスワード再設定フォームヘGETリクエストを送信させる記述を行う。 get edit_password_reset_path(user.reset_token, email: user.email) assert_template 'password_resets/edit' assert_select "input[name=email][type=hidden][value=?]", user.email #(10)無効なパスワードとパスワード確認を、PasswordResetsコントローラのupdateアクションヘPATCHリクエストを送信してしまった場合の記述をする。 patch password_reset_path(user.reset_token), params: { email: user.email, user: { password: "無効なパス", password_confirmation: "無効なパス確認" } } assert_select 'div#error_explanation' #(11)パスワードとパスワード確認入力が空のまま、PasswordResetsコントローラのupdateアクションヘPATCHリクエストを送信してしまった場合の記述をする。 patch password_reset_path(user.reset_token), params: { email: user.email, user: { password: "", password_confirmation: "" } } assert_select 'div#error_explanation' #(12)有効なパスワードとパスワード確認入力で、PasswordResetsコントローラのupdateアクションヘPATCHリクエストが送信された場合の記述をする。 patch password_reset_path(user.reset_token), params: { email: user.email, user: { password: "有効なパス", password_confirmation: "有効なパス確認" } } assert is_logged_in? assert_not flash.empty? assert_redirected_to user end end
最後に
以上で後編のパスワード再設定機能の本実装が完了しました。
前編→パスワード再設定用のリソース作成
中編→パスワード再設定メール送信機能の実装参考
- 投稿日:2020-04-21T22:43:17+09:00
mkcertを使いRailsのローカル開発環境をSSL化する方法(for Mac)
mkcertとは?
Railsの開発環境でHTTPSを有効にするためのツール。
Github( https://github.com/FiloSottile/mkcert#installation )には以下のように書かれています。mkcert is a simple tool for making locally-trusted development certificates. It requires no configuration. 訳:mkcertは、ローカルに信頼された開発証明書を作成するためのシンプルなツールです。設定は不要です。mkcertをインストールする
homebrewを使っている場合のインストールコマンド
$ brew install mkcert # ブラウザにFirefoxを使う場合は以下のコマンドも追加 $ brew install nss# ローカルCA(認証局)を作成 $ mkcert -install# 証明書を作成 $ mkcert localhost
上から順に説明すると
■"/Users/ユーザーの名前/library/Application Support/mkcert"にあるローカル認証局を使っています
■"localhost"という名前で新しい証明書を作成しました
■証明書は".localhost.pem"に、鍵は".localhost-key.pem"にあります
ということです。puma.rbの設定を変更
ポートと、証明書と鍵を追記しましょう。
(中略) if ENV.fetch('RAILS_ENV') { 'development' } == 'development' ssl_bind '0.0.0.0', '9292', key: './localhost-key.pem', cert: './localhost.pem' end (中略)以上でHTTPS接続の設定は完了
https://localhost:9292/ にアクセスして、接続が可能か確認します。
これだけで接続可能になるとは非常に簡単ですね。今回はMacのみのやり方について書きましたので、Mac以外の方はGithubにやり方が載っているのでそちらを参照ください。
参考
- 投稿日:2020-04-21T21:22:19+09:00
初心者には謎が多いrails db:migrateとは?
rails tutorialを25日間かけてやり終えた和己です。
かなり挫折しかけましたがなんとかやり終えました。
そこでわからない用語が複数あったので復習をかねてまとめていきたいと思います。今回はrailsチュートリアルで頻出する
rails db:migrateについてです
そもそもmigrationって?
migration(rails db:migrateコマンド)は、railsで使用するデータベースの構造(テーブル、カラム)を変更するときに利用する機能です。
データベースを変更するときの大まかな流れ
1.
$ rails g migration
コマンドでマイグレーションファイルを作成
2.マイグレーションファイルの中身を書き換える
3.$ rails db:migrate
コマンドでマイグレーションを順番に実行してデータベースに変更を加える基本
1. 空のマイグレーションファイルを作成する
# 空のmigrationファイルを作成する $ rails g migration create_user Running via Spring preloader in process 2198 invoke active_record create db/migrate/20200419035049_create_user.rb # 20200419035049というのがmigrationファイルが作られた日時 # マイグレーションの実行状況を確かめる $ rails db:migrate:status database: hogehoge/your_project Status Migration ID Migration Name -------------------------------------------------- down 20200419035049 CreateUser # statusが'down'となっているのはマイグレーションがまだ実行されていないことを示している↓マイグレーションと呼ばれる新しいファイルが生成されます。
# db/migrate/20200419035049_create_user.rbの中身 class CreateUser < ActiveRecord::Migration[5.2] def change end endマイグレーションファイル名の先頭には、それが生成された時間のタイムスタンプが追加されます。以前はインクリメンタルな整数が追加されましたが、複数の開発者によるチームでは、複数のプログラマが同じ整数を持つマイグレーションを生成してしまい、コンフリクトを引き起こしていました。現在のタイムスタンプによる方法であれば、まったく同時にマイグレーションが生成されるという通常ではありえないことが起きない限り、そのようなコンフリクトは避けられます。
2.migrationファイルを書いていく
class CreateUser < ActiveRecord::Migration[5.2] def change # rails db:migrateで実行されるメソッド create_table :users do |t| #usersという名前のテーブルを新たに作成 t.string :name #string型のnameカラムを作成 t.text :profile #text型のprofileカラムを作成 t.timestamps #created_at, updated_atを自動で作成 end end end3.マイグレーションファイルを実行してデータベースに変更を加える
$ rails db:migrate == 20200419035118 CreateUsers: migrating ====================================== -- create_table(:user) -> 0.0024s == 20200419035118 CreateUsers: migrated (0.0026s) ============================= $ rails db:migrate:status # マイグレーションの実行状況を確かめる database: hogehoge/your_project Status Migration ID Migration Name -------------------------------------------------- up 20200419035049 CreateUser # statusが'up'に変わり、マイグレーションが正常に実行されたことを示している1→2→3の流れで以下のようなデータ構造を持つUserテーブルが作成される
カラム名 データ型 id integer name string profile text created_at datetime updated_at datetime migrationのメリット、デメリット
メリット
・SQL文を書かずにデータベースの構造を変更できる
・migrationファイルを共有することで、複数の開発環境でデータベースの構造を簡単に共有できるデメリット
SQL書かずにデータベース作るから内容がわからないまま進めてしまう。
db:migrate
が実行されると、db/development.sqlite3という名前のファイルが生成されます。これはSQLiteデータベースの実体です。ほぼすべてのマイグレーションは、元に戻すことが可能
元に戻すことを「ロールバック (rollback)と呼び、Railsではdb:rollbackというコマンドで実現できます。
$ rails db:rollback
rails db:rollback
によってchangeメソッドに定義された処理が逆順に実行される。$ rails db:rollback == 20200419036118 CreateUsers: reverting ====================================== -- drop_table(:users) -> 0.0014s == 20200419036118 CreateUsers: reverted (0.0034s) ============================= $ rails db:migrate:status # マイグレーションの実行状況を確認 database: hogehoge/your_project Status Migration ID Migration Name -------------------------------------------------- down 20200419035049 CreateUser参考にした記事
Railsチュートリアル第6章
RailsのMigrationに関する基本まとめ
【Rails入門】データベースを設定するrails db:migrateを説明!余談
今回は記事を読んで頂きありがとうございました。
毎日Twitterも更新していますのでフォローして頂けると嬉しいです。
和己のTwitter
- 投稿日:2020-04-21T20:27:43+09:00
アップデートによりシェルスクリプトがbashからzshに変わった時の対処方法
まえがき
2020/3末、macをアップデートしてからrails sでサーバを立ち上げると、
Your Ruby version is 2.6.3, but your Gemfile specified 2.6.5となるので、
bundle installで、一旦はサーバーが立ち上がるようになるのですが、翌日などに立ち上げると、表示が出て立ち上がらなくなった
状況確認
① Rubyのバージョンの確認
ruby -vすると、
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin19]2.6.5はインストールしているのですが、2.6.3はインストールした記憶がない…(したのかな?)
② rbenvの確認
which rubyすると、
ユーザー名@mba desktop % which ruby /usr/bin/rubyこのように表示された場合、rbenvが反映されていません。システムのRubyを参照したままになっています。
③ シェルスクリプトの確認
echo $SHELL # 結果がbin/bashの場合 cat ~/.bash_profile # 結果がbin/zshの場合 cat ~/.zshrcすると、結果が、
bin/zshで、
cat ~/.zshrcしたところ、
cat: /Users/ユーザー名/.zshrc: No such file or directoryとなりました。
次に、
echo 'eval "$(rbenv init -)"' >> ~/.zshrcを実行後、再度、
cat ~/.zshrcを実行、結果は、
eval "$(rbenv init -)"原因
アップデートした際、シェルスクリプトがbashからzshに変わった事で異常が起きている。
対処法
①open ~/.zshrc
でファイルを開き、以下に置き換える。# Homebrew export PATH=/usr/local/bin:$PATH # Homebrew rbenv export PATH="$HOME/.rbenv/bin:$PATH" eval "$(rbenv init -)"その上で、次を実行。
source ~/.zshrc rbenv global 2.6.5② ターミナルを再起動して、
ruby -vを実行。結果は、
ruby 2.6.5p114 (2019-10-01 revision 67812)と表示されました。次に、
which rubyを実行。
/Users/ユーザー名/.rbenv/shims/rubyと表示されました。これでとりあえず大丈夫です。
あとがき
神里よしとさんの主宰するプログラミングのオンラインサロン「人生逆転サロン」のメンターのたけさんに質問して教えて頂いた流れを忘れないように、ここに書き留めておきます。
- 投稿日:2020-04-21T19:12:15+09:00
【Rails】seedから画像をdbに保存する方法
seedデータからdbに画像を保存するやり方です。
本番環境でもできます。(db/seeds.rb)
20.times do |n| name = Faker::Games::Pokemon.name email = Faker::Internet.email password = 'password' User.create!( name: name, email: email, password: password, password_confirmation: password, icon: open("./db/fixtures/icon#{n}.jpg") ) endこんな感じで
icon: open("./db/fixtures/icon#{n}.jpg")
で呼び出しています。
自分は(db/fixtures)の中にicon0.jpg
、icon1.jpg
みたいな感じで20個jpgファイルを置きました。
ご参考までに
- 投稿日:2020-04-21T18:34:13+09:00
Rails で PostgreSQL のパーテイションを利用したい
なにこれ?
Ruby on Rails で PostgreSQL のハッシュパーティションを使いたいよねー、という方!こうすればできました。たぶん、rails db:migrate or db:rollback したら DDL が発行されてよしなに設定されるはず。
サンプルコード
class CreateStaffs < ActiveRecord::Migration[6.0] def change reversible do |dir| dir.up do execute <<~"SQL" CREATE TABLE staffs (id uuid PRIMARY KEY UNIQUE, email varchar NOT NULL, cname varchar NOT NULL, created_at timestamp(6) NOT NULL, updated_at timestamp(6) NOT NULL) PARTITION BY HASH (id); #{(0..99).map{|num| "CREATE TABLE staffs_partition_#{num} PARTITION OF staffs FOR VALUES WITH(MODULUS 100,REMAINDER #{num});"}.join} SQL end dir.down do execute <<-SQL DROP TABLE staffs; SQL end end end endおわりに
PostgreSQL 11 からの機能なので、使えない環境の方もいらっしゃると思われます。が、あとで楽できそうなので必要に応じてパーテイションを作っておくことが良さそうですね。
- 投稿日:2020-04-21T18:34:13+09:00
Rails で PostgreSQL のパーティションを利用したい
なにこれ?
Ruby on Rails で PostgreSQL のハッシュパーティションを使いたいよねー、という方!こうすればできました。たぶん、rails db:migrate or db:rollback したら DDL が発行されてよしなに設定されるはず。
サンプルコード
class CreateStaffs < ActiveRecord::Migration[6.0] def change reversible do |dir| dir.up do execute <<~"SQL" CREATE TABLE staffs (id uuid PRIMARY KEY UNIQUE, email varchar NOT NULL, cname varchar NOT NULL, created_at timestamp(6) NOT NULL, updated_at timestamp(6) NOT NULL) PARTITION BY HASH (id); #{(0..99).map{|num| "CREATE TABLE staffs_partition_#{num} PARTITION OF staffs FOR VALUES WITH(MODULUS 100,REMAINDER #{num});"}.join} SQL end dir.down do execute <<-SQL DROP TABLE staffs; SQL end end end endおわりに
PostgreSQL 11 からの機能なので、使えない環境の方もいらっしゃると思われます。が、あとで楽できそうなので必要に応じてパーテイションを作っておくことが良さそうですね。
- 投稿日:2020-04-21T14:55:54+09:00
rails-erd 設定
- 投稿日:2020-04-21T14:51:22+09:00
[Rails]TwitterAPI認証+deviseでのログインの実装方法
経緯
railsにdeviseをインストールした状態でTwitterAPI認証でのログインを実装しました。
色んなサイトを参考にした結果、迷走し2日以上かけてしまいました。
今後も使うかもしれない自分用の備忘録的サムシングとして、またみなさんにササッと実装いただけたらと思い投稿しました。開発環境
ruby 2.7.0
rails 5.2.4
AWS EC2/RDS前提条件
・Twitterアカウントがある。
・deviseインストール済のアプリがある。
・環境変数の設定ができている。
・本番環境でアプリの動作ができている状態。TwitterのAPI認証取得および設定
a. API取得
https://qiita.com/kngsym2018/items/2524d21455aac111cdee
私はこちらのサイトを参考にしました。callbackURLには以下のように入れてください。
URL : https://[自分のサイトのリンク]/users/auth/twitter/callback
(https化されてない場合は、うまく動作しないかもしれません。)Terms of service URLとPrivacy policy URL何か埋めてください。次の項目で必須となってます。
こんな感じになります。
c. Permissionsの設定
read onlyなどは用途に合わせて選んでください。
ユーザー登録にメルアドが必要なので「Request email addresses from users」チェック
Gemの設定
Gemfilegem 'omniauth' gem 'omniauth-twitter'忘れずに
$ bundleAPI keyとAPI secretの設定
a. メモしたAPI keyとAPI secretを.envファイルに記述します。
ここではそれぞれの名前はTWITTER_CONSUMER_KEYとTWITTER_CONSUMER_SECRETにしています。
ドメインネーム(アプリのドメインorIPアドレス)も記述します.envTWITTER_CONSUMER_KEY = "****************************" TWITTER_CONSUMER_SECRET = "****************************" DOMAIN_NAME = "*****************"b. .gitignoreに記述
.gitignore. . .env . .deviseの設定
記述した認証用のキーをdeviseが識別できるように記述します。
config/initializers/devise.rbconfig.omniauth :twitter, ENV['TWITTER_CONSUMER_KEY'], ENV['TWITTER_CONSUMER_SECRET'], scope: 'email', oauth_callback: "#{ENV['DOMAIN_NAME']}/users/auth/twitter/callback" OmniAuth.config.logger = Rails.logger if Rails.env.development? # debug用UserモデルとTwitter認証モデルを紐付ける
a. deviseに:omniauthable, omniauth_providers: [:facebook, :twitter, :google_oauth2]を追加
facebookとグーグル認証も将来的に使うかもしないので一応付けておきます。app/models/user.rbdevise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, # 外部API認証用に以下追加 :omniauthable, omniauth_providers: [:facebook, :twitter, :google_oauth2]b. マイグレーションファイルにカラムの追加
$ rails g migration AddColumnsToUsers uid:string provider:string $ rails db:migratec. Userモデルにfindメソッドを実装
app/models/user.rb# Twitter認証ログイン用 # ユーザーの情報があれば探し、無ければ作成する def self.find_for_oauth(auth) user = User.find_by(uid: auth.uid, provider: auth.provider) user ||= User.create!( uid: auth.uid, provider: auth.provider, name: auth[:info][:name], email: User.dummy_email(auth), password: Devise.friendly_token[0, 20] ) user end def self.dummy_email(auth) "#{Time.now.strftime('%Y%m%d%H%M%S').to_i}-#{auth.uid}-#{auth.provider}@example.com" endメールアドレスでの認証も実装している場合は、OAuthでの認証時もメールアドレスを保存する必要があります。
emailはダミー用のアドレスを作成して代用します。
email中に時間まで記載しているのは、認証後に退会などで論理削除された後も再度認証ログインできるように今回は設定しています。d. omniauthコントローラーの設定
deviseのusers配下にあります。
無い方は$ rails g devise:controllers users
を実行すると見えるようになります。app/controllers/users/omniauth_callbacks_controller.rbdef twitter callback_from :twitter end private # コールバック時に行う処理 def callback_from(provider) provider = provider.to_s @user = User.find_for_oauth(request.env['omniauth.auth']) # persisted?でDBに保存済みかどうか判断 if @user.persisted? # サインアップ時に行いたい処理があればここに書きます。 flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: provider.capitalize) sign_in_and_redirect @user, event: :authentication else session["devise.#{provider}_data"] = request.env['omniauth.auth'] redirect_to new_user_registration_url end endルーティングの設定
omniauth_callbacks_pathの追加
config/routes.rbdevise_for :users, :controllers => { :registrations => 'users/registrations', :sessions => 'users/sessions', :passwords => 'users/passwords', # このpathを通して外部API認証が行われる。 :omniauth_callbacks => 'users/omniauth_callbacks' }ビューに認証用リンクの設定
書き方はなんでもありだと思いますが一例として
a. loginページにリンク追加app/views/devise/registrations/new.html.erb<%= link_to user_twitter_omniauth_authorize_path do %> <i class="fab fa-twitter-square fa-2x"></i> <% end %>b. signupページにリンク追加
app/views/devise/sessions/new.html.erb<%= link_to user_twitter_omniauth_authorize_path do %> <i class="fab fa-twitter-square fa-2x"></i> <% end %>c. cssにtwitterアイコンのカラーを追加
app/assets/stylesheets/application.css.scss.fa-twitter-square { color: #1da1f2; }本番環境で動作確認
ここまでの操作でできているはずです。
a. アップロードの準備
.envのアップロード
ローカル環境でアプリケーション直下に移動して$ scp -i ~/.ssh/practice-aws.pem .env ec2-user@××.××.×××.×××:[アプリ名]/本番環境に入って、
// プリコンパイルを実行 $ bundle exec rails assets:precompile RAILS_ENV=production // マイグレートを実行 $ bundle exec rails db:migrate RAILS_ENV=production // サーバー起動 $ rails s -e productionb. ビューの確認
ツイッターのアイコンが表示出来ています。c. 認証ログイン後確認
ツイッターでの表示名でログインできていることが確認できました。まとめ
TwitterAPIを使ってワンクリックでサインアップすることができるようになったため、よりユーザービリティが上がったと思います。
はじめてAPI認証でのログインに挑戦してみて、WEBのつながりが見えてきて非常に勉強になりました。今回時間がかかった要因
a. ローカルでの試験運用ができなかったこと
TwitterDeveloperのwebsiteURLに他サイトで推奨されているようなhttp://127.0.0.1:3000 などを入れても「invalid URL」といったメッセージが返ってくるため、(他のやりようがあったみたいですが少々面倒だったので)ローカル→本番→試験→NGを繰り返していました。結果的にはめちゃめちゃ時間くっちゃいましたので、面倒でもローカルでの有効化方法を模索すればよかったと反省です。良い方法があれば教えていただけると嬉しいです。
b. deviseでの認証の場合、やり方が通常と異なること
通常では/config/initializers下にomniauth.rbというファイルを新規作成してコントローラーも別で作ってってやり方になると思いますが、そちらでやっていたためdeviseが入ってるとログイン認証はデバイスが絡んでいるため、エラーになっていました。
deviseの便利さに甘えて、大事なことに気づくことが遅れてしまいました。反省です。参考
・https://qiita.com/kazuooooo/items/47e7d426cbb33355590e
・https://qiita.com/zenizh/items/94aec2d94a2b4e9a1d0b
・https://qiita.com/puremoru0315/items/f1d459b663fd3b715dee
・https://kurose.me/twitter-omniauth/#Twitter
- 投稿日:2020-04-21T13:34:33+09:00
メモランダム Rails tutorial 第7章
Rails tutorial 第8章
ユーザー登録
- いよいよユーザー登録機能を追加しましょう
- 7.2ではHTML フォームを使ってWebアプリケーションに登録情報を送信
- 7.4ではユーザーを新規作成して情報をデータベースに保存
- ユーザーを表示するためのページを作成し、ユーザー用のRESTアーキテクチャを実装する第一歩を踏み出します
1.ユーザーを表示する
- モックアップ
デバッグとRails環境
- ビルトインのdebugメソッドとparams変数を使って、各プロフィールページにデバッグ用の情報が表示されるようになります
<%= debug(params) if Rails.env.development? %>
- デバッグ情報は開発環境以外で表示させないべき
2.Usersリソース
resources :usersという行は、ユーザー情報を表示するURL (/users/1) を追加するためだけのものではありません。
- サンプルアプリケーションにこの1行を追加すると、ユーザーのURLを生成するための多数の名前付きルート (5.3.3) と共に、RESTfulなUsersリソースで必要となるすべてのアクションが利用できるようになるのです
ーザーのid読み出しにはparamsを使いました。
- Usersコントローラにリクエストが正常に送信されると、params[:id]の部分はユーザーidの1に置き換わります
debuggerメソッド
- 直接的にデバッグする方法もあります。それがbyebug gemによるdebuggerメソッドです
Gravatar画像とサイドバー
- 今度は各ユーザーのプロフィール写真のあたりをもう少し肉付けし、サイドバーも作り始めましょう
Gravatarは無料のサービスで、プロフィール写真をアップロードして、指定したメールアドレスと関連付けることができます
gravatar_forヘルパーメソッドを使ってGravatarの画像を利用できるようにします
Rubyでは、Digestライブラリのhexdigestメソッドを使うと、MD5のハッシュ化が実現できます。
2.ユーザー登録フォーム
- モックアップ確認
form_tagを使用する
- Railsでform_forヘルパーメソッドを使います
- このメソッドはActive Recordのオブジェクトを取り込み、そのオブジェクトの属性を使ってフォームを構築します。
- ユーザーオブジェクトの作成
- 登録フォーム
フォームHTML
- doキーワードは、 form_forが1つの変数を持つブロックを取ることを表します。この変数fは “form” のfです。
- Railsヘルパーを使っている場合、実装の詳細について知っておく必要はありません。
- ただしfというオブジェクトが何をするのかは知っておく必要があります。
- このfオブジェクトは、HTMLフォーム要素 (テキストフィールド、ラジオボタン、パスワードフィールドなど) に対応するメソッドが呼び出されると、@userの属性を設定するために特別に設計されたHTMLを返します
3.ユーザー登録失敗
- フォームを理解するにはユーザー登録の失敗のときが最も参考になります
正しいフォーム
- createアクションでフォーム送信を受け取り、User.newを使って新しいユーザーオブジェクトを作成し、ユーザーを保存 (または保存に失敗) し、再度の送信用のユーザー登録ページを表示するという方法で機能を実装しようと思います
- render メソッドを再度使いまわし
Strong Parameters
- user_paramsという外部メソッドを使うのが慣習になっています
@user = User.new(user_params)
エラーメッセージ
- Railsは、このようなメッセージをUserモデルの検証時に自動的に生成してくれます
- errors.full_messagesオブジェクトは、 エラーメッセージの配列を持っています
'shared/error_messages'というパーシャルをrender (描画) している点に注目
any?メソッドはちょうどempty?と逆の動作で、要素が1つでもある場合はtrue、ない場合はfalseを返します
pluralizeの最初の引数に整数が与えられると、それに基づいて2番目の引数の英単語を複数形に変更したものを返します。このメソッドの背後には強力なインフレクター (活用形生成) があり、不規則活用を含むさまざまな単語を複数形にすることができます。
- これにより、"1 errors" のような英語の文法に合わない文字列を避けることができます (これはWeb上でどうしようもないほどよく見かけるエラーです)。
失敗時のテスト
- 有効な送信をしたときの正しい振る舞いについてテストを書いていきます
- 色々(パス)
4.ユーザー登録成功
- まずは、ユーザーを保存できるようにします
登録フォームの完成
- Railsの一般的な慣習に倣って、ユーザー登録に成功した場合はページを描画せずに別のページにリダイレクト (Redirect) するようにしてみましょう
- redirect_to メソッド
flash
- 登録完了後に表示されるページにメッセージを表示し (この場合は新規ユーザーへのウェルカムメッセージ)、2度目以降にはそのページにメッセージを表示しないようにするというものです
flash[:success] = "Welcome to the Sample App!"
実際のユーザー登録
- データベースの内容を一旦リセット
成功時のテスト
- 今回はassert_differenceというメソッドを使ってテストを書きます
- このメソッドは第一引数に文字列 ('User.count') を取り、assert_differenceブロック内の処理を実行する直前と、実行した直後のUser.countの値を比較します。第二引数はオプションですが、ここには比較した結果の差異 (今回の場合は1) を渡します。
5.プロのデプロイ
このアプリケーションをデプロイして、本番環境でも動かせるようにしてみましょう
- 実際にデータを操作できるようにするデプロイは初めて
デプロイの下準備として、まずはこの時点までの変更をmasterブランチにマージしておいてください。
本番環境でのSSL
このようなネットワークに流れるデータは途中で捕捉できるため、扱いには注意が必要です。これはサンプルアプリケーションの本質的なセキュリティ上の欠陥です。
- そしてこれを修正するためにSecure Sockets Layer (SSL)を使います 12。これはローカルのサーバからネットワークに流れる前に、大事な情報を暗号化する技術です
production.rbという本番環境の設定ファイルの1行を修正するだけで済みます。具体的には、configに「本番環境ではSSLを使うようにする」という設定をするだけです
本番環境用のWebサーバー
- WEBrickは本番環境として適切なWebサーバではありません。よって、今回はWEBrickをPumaというWebサーバに置き換えてみます
- 新しいWebサーバを追加するために、Heroku内のPumaドキュメント (英語) に従ってセットアップしていきます。
本番環境へのデプロイ
- 変更をコミットし、デプロイしてみましょう
- ** 次の点は頭の片隅に置いておいてください。それは、仕事でHerokuを使ったアプリケーションを動かす場合はGemfileでRubyのバージョンを明示しておいた方が賢明である、という点です**
6.最後に
- 第8章と第9章では、認証 (authentication) システムを導入し、ユーザーがログインとログアウトをできるようにします ([remember me] という発展的な機能も実装します)。
本章のまとめ
- debugメソッドを使うことで、役立つデバッグ情報を表示できる
- Sassのmixin機能を使うと、CSSのルールをまとめたり他の場所で再利用できるようになる
- Railsには標準で3つ環境が備わっており、それぞれ開発環境 (development)、テスト環境 (test)、本番環境 (production)と呼ぶ
- 標準的なRESTfulなURLを通して、ユーザー情報をリソースとして扱えるようになった
- Gravatarを使うと、ユーザーのプロフィール画像を簡単に表示できるようになる
- form_forヘルパーは、Active Recordのオブジェクトに対応したフォームを生成する
- ユーザー登録に失敗した場合はnewビューを再描画するようにした。その際、Active Recordが自動的に検知したエラーメッセージを表示できるようにした
- flash変数を使うと、一時的なメッセージを表示できるようになる
- ユーザー登録に成功すると、データベース上にユーザーが追加、プロフィールページにリダイレクト、ウェルカムメッセージの表示といった順で処理が進む
- 統合テストを使うことで送信フォームの振る舞いを検証したり、バグの発生を検知したりできる
- セキュアな通信と高いパフォーマンスを確保するために、本番環境ではSSLとPumaを導入した
- 投稿日:2020-04-21T12:58:22+09:00
Railsでstubを使う
よく使う構文をメモ
post = Post.new # 引数なし allow(post).to receive(:title).and_return('title') # 引数あり allow(post).to receive(:title).with(arg1, arg2).and_return('title') # エラーを起こさせる allow(post).to receive(:subtitle).and_raise(StandardError.new) # クラスに対してインスタンスメソッドのスタブ化 allow_any_instance_of(Post).to receive(:title).and_return('title')参考
- 投稿日:2020-04-21T12:36:17+09:00
メモランダム Rails tutorial 第6章
Rails tutorial 第6章
ユーザーのモデルを作成する
- 認証 (authentication) と認可 (authorization) のシステムの例だと、Clearance、Authlogic、Devise、CanCanなど
1. Userモデル
- 情報を保存するためのデータ構造を作成する
- Railsでは、データモデルとして扱うデフォルトのデータ構造のことをモデル (Model) と呼びます
- データベースとやりとりをするデフォルトのRailsライブラリはActive Recordと呼ばれます
データベースの移行
- この節での目的は、簡単に消えることのないユーザーのモデルを構築すること
- nameとemailの2つの属性からなるユーザーをモデリングするところから始める
attr_accessor :name, :email
は必要ない- generate modelというコマンドを使います -
$ rails generate model User name:string email:string
modelファイル
- この節では、以後このモデル用ファイルを理解することに専念します
ユーザーオブジェクトを作成する。
- Active Recordを理解する上で、「有効性 (Validity)」という概念も重要
- データベースにデータがあるかどうかは有効性には関係ありません
- Userモデルのインスタンスはドット記法を用いてその属性にアクセスすることができます
- create, destroy
ユーザーオブジェクトを検索する
User.find(3)
User.find_by(email: "mhartl@example.com")
User.first
User.all
ユーザーオブジェクトを更新する。
- 基本的な更新の方法は2つ
- 属性を個別に代入する方法
- update_attributesを使うケース
user.update_attribute(:name, "El Duderino")
2.ユーザーを検証する。
- nameとemailにあらゆる文字列を許すのは避けるべき
- 存在性 (presence)の検証、
- 長さ (length)の検証、
- フォーマット (format)の検証、
- 一意性 (uniqueness)の検証
有効性を検証する。
存在性を検証する
- 「存在性 (Presence)」。これは単に、渡された属性が存在することを検証する。
- name属性にバリデーションに対するテスト
assert_not @user.valid?
- name属性の存在性を検証する
validates :name, presence: true
validates(:name, presence: true)
user.errors.full_messages
長さを検証する
- nameの長さの検証に対するテスト
user.name = "a" * 51
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 }
フォーマットを検証する
ここではメールアドレスにおなじみのパターンuser@example.comに合っているかどうかも確認することを要求します
- %w[]という便利なテクニック
-assert @user.valid?, "#{valid_address.inspect} should be valid"
このオプションは引数に正規表現 (Regular Expression) (regexとも呼ばれます) を取ります。正規表現は一見謎めいて見えますが、文字列のパターンマッチングにおいては非常に強力な言語です。つまり、有効なメールアドレスだけにマッチして、無効なメールアドレスにはマッチしない正規表現を組み立てる必要があります
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
format: { with: VALID_EMAIL_REGEX }
一意性を検証する
- validatesメソッドの:uniqueオプションを使います
test "email addresses should be unique" do duplicate_user = @user.dup @user.save assert_not duplicate_user.valid? end
- @userと同じメールアドレスのユーザーは作成できないことを、@user.dupを使ってテストしています
- emailのバリデーションにuniqueness: trueというオプションを追加
3.セキュアなパスワードを追加する
- 、ハッシュ関数を使って、入力されたデータを元に戻せない (不可逆な) データにして保存する。
ハッシュ化されたパスワード
- セキュアなパスワードの実装は、has_secure_passwordというRailsのメソッドを呼び出すだけでほとんど終わってしまいます
- 今回の用途ではハッシュ化されたパスワードと暗号化されたパスワードは類義語となります
- has_secure_passwordを使ってパスワードをハッシュ化するためには、最先端のハッシュ関数であるbcryptが必要になります。パスワードを適切にハッシュ化することで、たとえ攻撃者によってデータベースからパスワードが漏れてしまった場合でも、Webサイトにログインされないようにできます。サンプルアプリケーションでbcryptを使うために、bcrypt gemをGemfileに追加します
パスワードの最小文字数
test "password should be present (nonblank)" do
@user.password = @user.password_confirmation = " " * 6
assert_not @user.valid?
end
test "password should have a minimum length" do
@user.password = @user.password_confirmation = "a" * 5
assert_not @user.valid?
end
validates :password, length: { minimum: 6 }
validates :password, presence: true, length: { minimum: 6 }
ユーザーの作成と認証
- has_secure_passwordをUserモデルに追加したことで、そのオブジェクト内でauthenticateメソッドが使えるようになっています
4.最後に
- たった12行でここまでの機能が実装できたことは、Railsの注目に値する特長でもあります
本章のまとめ
- マイグレーションを使うことで、アプリケーションのデータモデルを修正することができる
- Active Recordを使うと、データモデルを作成したり操作したりするための多数のメソッドが使えるようになる
- Active Recordのバリデーションを使うと、モデルに対して制限を追加することができる
- よくあるバリデーションには、存在性・長さ・フォーマットなどがある
- 正規表現は謎めいて見えるが非常に強力である
- データベースにインデックスを追加することで検索効率が向上する。また、データベースレベルでの一意性を保証するためにも使われる
- has_secure_passwordメソッドを使うことで、モデルに対してセキュアなパスワードを追加することができる
- 投稿日:2020-04-21T11:22:48+09:00
メモランダム Rails tutorial 第5章
Rails tutorial 第5章
レイアウトを作成する
- apurike-syonnnibootstrapフレームワークを組み込み、カスタムスタイルを追加する。
- home, aboutへのリンクをレイアウトに追加。
- パーシャル、 railsのルーティング、Asset pipelineについて学ぶ。+ Sass。ユーザーログインへの第一歩も。
1.構造を追加する
- webアプリケーション = ユーザインターフェース
- モックアップをスケッチ。
- この時点でブランチを作成。
ナビゲーション
- サイトのレイアウトがいるに HTMLを追加。
- internetExplorerのHTML5サポート不備を回避するJavaScriptのHTML5shimというコード。
header
- navbar、navbar-fixed-top、navbar-inverse
classラベルは何度も使える。
idラベルは一度しか使えない。
container
link_to
オプションハッシュ#
navタグには「その内側がナビゲーションリンクである」という意図を明示的に伝える役割
yieldメソッドはWebサイトのレイアウトにページごとの内容を挿入する。
link_to の image_tag
- app/assets/images/ディレクトリの中から
alt属性は、画像がない場合に代わりに表示される文字列
BootstrapとカスタムCSS
Bootstrapを使うことでアプリケーションをレスポンシブデザイン (Responsive Design) にできる
どの端末でアプリケーションを閲覧しても、ある程度見栄えをよくすることができ
- Gemfileにbootstrap-sassを追加する
- bundle installを実行
- カスタムCSSを動かすための最初の一歩は、カスタムCSSファイルを作ること
Sassは5.2.2まで登場しませんが、bootstrap-sass gemが動作するためのおまじないとして必要
- Webサーバを再起動させると、アプリケーションに反映させる
Ctrl-Cを押してWebサーバを停止させた後、rails serverコマンドを打ってWebサーバを起動してください
- navbar-fixed-top
- navbar-inverse
- text-align: center
- color: #fff
パーシャル partial
一箇所にまとめた方が便利
- Railsではパーシャル (partial) という機能でこのような課題を解決できる
- renderと呼ばれるRailsヘルパー
2.Sassとアセットパイプライン
- 最近のRailsに追加された機能の中で最も特筆すべき機能の1つは、CSS、JavaScript、画像などの静的コンテンツの生産性と管理を大幅に強化する「アセットパイプライン (Asset Pipeline)」
アセットパイプライン
アセットディレクトリ
- 3種 app, lib , vendor
マニフェストファイル
- ニフェストファイルを使って、アセットをどのように1つのファイルにまとめるのかをRailsに指示する
プリプロセッサエンジン
- プリプロセッサエンジンは、繋げて実行する (chain) ことができます
Sass-素晴らしい構文を備えたスタイルシート
ネスト = ルールを継承
- :hoverなど
bootstrap-sassというgemを使えば、SCSSでも同様に$gray-lightという変数が使える
3.レイアウトのリンク
Contactページ
'test "should get contact" do
rails test`
- Contact用ページのルートとアクション,ビューを追加する。
- 全てのテストがグリーンであることを確認。railsのルートURL
- 一般的な規約に従い、基本的にはpath書式を使い、リダイレクトの場合のみurl書式を使うようにします
名前付きルート
- link_toメソッドの2番目の引数で、適切な名前付きルートを使ってみましょう
リンクのテスト
- これらのリンクが正しく動いているかどうかチェックするテストを書いてみましょう
- 「統合テスト (Integration Test)」を使って一連の作業を自動化
4.ユーザー登録:最初のステップ
- ユーザーのモデリングは第6章、ユーザ登録の完成は第7章。
Usersコントローラ
- Usersコントローラを作成
$ rails generate controller Users new
ユーザー登録用URL
- ユーザー登録URL用にget '/signup'のルートを追加
5.最後に
- 以後サンプルアプリケーションを肉付けすることに専念します。最初に、ユーザー登録、サインイン、サインアウトできるユーザーを追加します。次に、マイクロポストを追加します。最後に、他のユーザーをフォローできるようにします。
- Gitを使っている方は、この時点でmasterブランチに変更をマージしてください。
本章のまとめ
- HTML5を使ってheaderやfooter、logoやbodyといったコンテンツのレイアウトを定義
- Railsのパーシャルは効率化のために使われ、別ファイルにマークアップを切り出すことができます
- CSSは、CSSクラスとidを使ってレイアウトやデザインを調整します
- Bootstrapフレームワークを使うと、いい感じのデザインを素早く実装できる
- SassとAsset Pipelineは、(開発効率のために切り分けられた) CSSの冗長な部分を圧縮し、本番環境に最適化した結果を出力する
- Railsのルーティングでは自由にルールを定義することができ、また、その際に名前付きルートも使えるようになる
- 統合テストは、ブラウザによるページ間の遷移を効率的にシミュレートする
- 投稿日:2020-04-21T11:00:51+09:00
railsで一覧ページに[gem kaminari]を導入する流れについてまとめてみた。
kaminariの導入の仕方
1.Gemfileにkaminariを追加して保存。
Gemfile.gem 'kaminari','~> 1.1.1'2.kaminariをインストールする
$ bundle install
3.kaminariの設定ファイルを作成する
$ rails g kaminari:config
4.kaminariがページャで利用するテンプレートを作成する
$ rails g kaminari:views default
5.ページャを実装する
ページネーションさせたい場所に
app/views/books/index.html.erb<%= @books.each do |book| %> : : <% end %> <%= paginate @books %> <"←これを追加">app/controllers/books_controller.rb@books = Books.all.page(params[:page]).per(10) <"←.page以降追加">この場合1ページの表示件数が10件まで表示される。
kaminariにBootstrapを適用させる
Bootstrapをすでに導入していれば簡単にkaminariに適用させることができる。
$ rails g kaminari:views bootstrap3
これでapp/views/kaminariフォルダにBootstrap用のViewが生成され、Bootstrap用のテンプレートに適用される。
参考サイト
- 投稿日:2020-04-21T09:47:31+09:00
YouTubeっぽいコメント欄を作ってみよう!(返信機能追加)
はじめに
こんにちは!今回はYouTubeっぽいコメント欄を作ってみようと思います!
色々調べていたのですが、似たような記事がなかったので自力で実装した部分が多いです(><)
もっと良い方法があるかもしれないので、今後アップグレードしていければと思います。それではいきましょう!
完成イメージ
ユーザーアイコンの実装については過去記事を参照ください。
https://qiita.com/naoki00m/items/6430ab0b62766b582c9a開発環境
- ruby 2.5.1
- Rails 5.2.3
- mysql
- Haml 5.1.2
- Ruby Sass 3.7.4
前提条件
- 投稿に紐づくコメント機能が実装できていること(TECH CAMPでいうところのPictweetコメント機能)
実装内容
どのコメントに対する返信かを保存するカラムを追加
(Commentテーブルにreplyカラムを追加)コメントに対する返信フォームを作成
(form_withを使用)返信フォームを表示するためのボタンを作成
(FontAwesomeを使用)返信数をコメントの下に表示
(replyカラム内の値を計算)返信数をクリックして返信を閲覧表示
テーブル
Comment テーブル
Column Type Options user_id integer null: false video_id integer null: false text text null: false reply integer Assosiation
- belongs_to :user
- belongs_to :video
実装手順
1. どのコメントに対する返信かを保存するカラムを追加
Commentテーブルにreplyカラムを追加します。これはどのコメントに対する返信かを表すカラムです。
id:1のコメントに対してid:2で返信した場合、idが2のレコードにあるreplyカラムに1が保存されるといった具合です。ターミナル.$ rails g migration AddReplyToComment reply:integer生成されたマイグレーションファイルを反映させましょう。
ターミナル.$ rails db:migrate2. コメントに対する返信フォームを作成 + 返信フォームを表示するためのボタンを作成
まずはビューの実装から!
今回はVideoのshowアクション内でコメントを表示しています。一般的にはPostのshowアクションと同義です。
show.html.haml(省略) - @comments.each do |comment| -# replyカラムがnull(空)の場合(返信ではない場合) - if comment.reply.blank? .content__show__comment__bottom__top .content__show__comment__bottom__top__user -# ユーザーアイコンを表示 .content__show__comment__bottom__top__user__icon = link_to "/users/#{comment.user.id}" do - if comment.user.image.present? = image_tag comment.user.image.url, width: '100%' - else = image_tag('/images/default_user.jpg', width: '100%') -# ユーザー名を表示 .content__show__comment__bottom__top__user__name = link_to "/users/#{comment.user.id}" do = comment.user.name -# 返信ボタン、削除ボタンを表示 .content__show__comment__bottom__top__icons -# コメントした本人であれば返信ボタンと削除ボタンを表示 - if user_signed_in? && current_user.id == comment.user.id .content__show__comment__bottom__top__icons__reply = link_to '#' do = icon('fa', 'reply', id: "reply-btn#{comment.id}") .content__show__comment__bottom__top__icons__delete = link_to video_comment_path(comment.video_id, comment.id), method: :delete do = icon('fa', 'trash-alt') -# ログインしているがコメントした人でない場合返信ボタンのみ表示 - elsif user_signed_in? && current_user.id != comment.user.id .content__show__comment__bottom__top__icons__reply = link_to '#' do = icon('fa', 'reply', id: "reply-btn#{comment.id}") -# それ以外は何も表示しない - else = '' -# ユーザーがログインしている場合のみ返信できるようにする - if user_signed_in? .content__show__comment__bottom__reply -# idをcomment.idに応じて変化させる = form_with(model: [@video, @new_comment], local: true, id: "reply-form#{comment.id}") do |f| = f.text_area :text, placeholder: "Add a public reply...", id: "reply-text" -# 画面上には表示しないが、裏でreplyカラムに保存させるためにhidden_fieldを用いる! = f.hidden_field :reply, value: comment.id .content__show__comment__bottom__reply__btn .content__show__comment__bottom__reply__btn__cansel = 'Cansel' = f.submit "Reply", class: 'content__show__comment__bottom__reply__btn__send'そしたらJqueryでコメントのフォームに動きをつけます!
- 入力フォーム選択時にCansel, Replyボタンを表示する
- Canselボタン選択時に入力フォームを非表示にする
ファイル名は◯◯.jsになっていればなんでもOKです!
video-comment.js// 返信フォームの表示・非表示切り替え $(function() { // replyという配列に1から1000までを順番に格納していく // 本当はビューやコントローラーからcomment.idを取ってきて格納できるのが理想 var reply = []; for ( var i = 1 ; i <= 1000 ; i++ ) { reply.push(i); } // eachでreplyの中から1つずつ値を取り出しビューで定義したid名に付与 $.each(reply, function(index, value) { // 入力フォームは初め隠しておく $("#reply-form" + value).hide(); // 返信ボタンをクリックしたら入力フォームを表示 $("#reply-btn" + value).on("click", function() { $("#reply-form" + value).show(); }); // Canselボタンをクリックしたら入力フォームを非表示に $(".content__show__comment__bottom__reply__btn__cansel").on("click", function() { $("#reply-form" + value).hide(); }); }); });3. 返信数をコメントの下に表示
返信ができるようになったので試しに返信してみましょう!
テーブルのreplyカラムにidが保存されているか確認してください。
ビューに記載した「if comment.reply.blank?」を外すと返信も表示されますが、
コメントの下に紐づくような感じで表示はまだできていません。replyカラム内の値を計算しよう!
さて、replyカラムに保存できるようになりましたが、コメントに対して何件の返信があるかを表示するには
コントローラーでこんな記述をする必要があります。videos_controller.rbclass VideosController < ApplicationController (省略) def show (省略) @new_comment = Comment.new @comments = @video.comments.includes(:user) # Commentテーブルのreplyカラムの数をカウント @replies = Comment.group(:reply).count end end試しにビューファイルで下記のように記載してみるとこのように表示されます。
show.html.haml= @replies左側がreplyカラムの中身、右側がその合計数になります。
今回の例でいくとコメントが2件あり、コメント1に対する返信が3件、コメント2に対する返信が1件あるといった状況です。{nil=>2, 1=>3, 2=>1}この@repliesは配列の形になっているので下記のように書いてあげると合計数だけを取り出すことができます。
show.html.haml= @replies[1]3これを用いて返信数を実装していきます!
show.html.haml(省略) .content__show__comment__bottom__view-btn -# 返信がなければ - if @replies[comment.id] == nil = "" -# 返信が1件なら - elsif @replies[comment.id] == 1 = "View #{ @replies[comment.id] } reply" -# 返信が2件以上なら - else = "View #{ @replies[comment.id] } replies"4. 返信数をクリックして返信を閲覧表示
最後に返信をそれぞれのコメントの下に表示できるようにしよう!
ここで重要となるのはコメントをeach doで繰り返し表示している中で
更にreplyを繰り返し表示させるということ。
こうすることでそれぞれのコメントの下に返信が紐づいているように見えます。show.html.haml-# 前の実装 - @comments.each do |comment| - if comment.reply.blank? (省略) -# ここから実装 -# replyという変数を用意して、再度繰り返し表示 - @comments.each do |reply| -# (重要)replyカラムがcomment.idと同じ場合のみ表示する - if reply.reply == comment.id .content__show__comment__bottom__view-reply .content__show__comment__bottom__view-reply__top .content__show__comment__bottom__view-reply__top__user .content__show__comment__bottom__view-reply__top__user__icon = link_to "/users/#{reply.user.id}" do - if reply.user.image.present? = image_tag reply.user.image.url, width: '100%' - else = image_tag('/images/default_user.jpg', width: '100%') .content__show__comment__bottom__view-reply__top__user__name = link_to "/users/#{reply.user.id}" do = reply.user.name .content__show__comment__bottom__view-reply__top__icons - if user_signed_in? && current_user.id == reply.user.id .content__show__comment__bottom__view-reply__top__icons__delete = link_to video_comment_path(reply.video_id, reply.id), method: :delete do = icon('fa', 'trash-alt') - else = '' .content__show__comment__bottom__view-reply__text // 改行を含んだテキストも表示できるようにsimple_formatを使用 = simple_format(reply.text)Jqueryで動きを加えて完了です!
video-comment.js// 返信の表示・非表示切り替え $(function() { $(".content__show__comment__bottom__view-reply").hide(); $(".content__show__comment__bottom__view-btn").on("click", function() { // toggleにすると同じボタンを押したときに表示・非表示を切り替えてくれる $(".content__show__comment__bottom__view-reply").toggle(); }); });コード量は少し長いですが、ポイントを掴めれば実装できると思うので是非真似してみてください!
今後の追加実装予定
- 返信を非表示にするときは「View」ではなく「Hide」にする
- メンションをつけられるようにする
- コメント・返信した際に通知としてストックされるようにする
参考
https://qiita.com/rockguitar67/items/4fd28cba0c243a8d0ba5
https://pikawaka.com/rails/count
- 投稿日:2020-04-21T09:22:46+09:00
コーディング未経験のPO/PdMのためのRails on Dockerハンズオン、Rails on Dockerハンズオン vol.16 - Deploy to EKS -
はじめに
こんにちは!今回でラストです!
今回はAWSのマネージドKubernetesサービスであるElastic Kubernetes Service(EKS)にデプロイしてみたいと思います。
今まで作ってきたRailsアプリコンテナをEKSで動かし、DBは同じくAWSのマネージドRDBサービスのRelational Database Service(RDS)を使います。
インフラ構築にはInfrastructure as CodeツールのTerraformを使ってみます。あくまで「こいつ、動くぞ!」を目的にしているので、今回のハンズオンだけでこれらの全てを伝えるわけではありませんし、使いこなせるわけではありません。あくまでとっかかりとして捉えてみてください。
気になる方はどんどん調査して使ってみてください!AWSを使うので、各自AWSアカウントは取得しておいてくださいね。
では、最後のハンズオンを始めます!
前回のソースコード
ここまでのソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。
Kubernetes
Kubernetes(k8s)はコンテナオーケストレーションツールに位置付けられるOSSです。
コンテナオーケストレーションツールは、Dockerなどのコンテナ技術を使って作られたアプリケーションのデプロイ、スケーリング、サービスディスカバリー、負荷分散などなどを管理したり自動化したりできるもので、Docker単体だけでは難しかったコンテナの本番環境稼働を可能にしてくれます。(Docker単体ではボリュームやネットワークがサーバーと1対1だったり、サーバーとコンテナの関連の管理がおよそ人ではできなかったり困難がありました。)コンテナオーケストレーションツールとしては、Kubernetesの他にもDocker SwarmやMesosphereなどがありましたが、2020年現在、事実上Kubernetesがデファクトスタンダードとなっています。(参考:国内でDockerコンテナを本番利用している企業は9.2%、コンテナオーケストレーションツールはKubernetesがデファクト - ITmedia NEWS)
マネージドKubernetes
KubernetesはOSSなので誰でも自分のサーバーで環境構築することができます。
ただ、すごいことをやってくれるので仕組みもけっこう複雑です。「Kubernetesがデファクトか!じゃあ構築するぞ!」みたいなノリではできないんじゃないかと思います。
Kubernetesでは、全体をクラスターと呼び管理しています。クラスターを管理する部分を「マスターコンポーネント」、アプリコンテナが稼働する部分を「ノードコンポーネント」と分けたりします。(参考:Kubernetesのコンポーネント - Kubernetes)
特に「マスターコンポーネント」はクラスターのコントロールプレーンを担っておりとても重要かつ要素も複雑です。運用したくないです。そこで大手クラウドベンダーはマネージドKubernetesサービスとして、利用者は主にノードコンポーネントの一部(インスタンス数とかボリュームとか)だけに関心を持っていればKubernetesを使えるサービスを展開しています。
AWSであればElastic Kubernetes Service(EKS)、GCPであればGoogle Kubernetes Engine(GKE)、Microsoft AzureであればAzure Kubernetes Service(AKS)です。今回はEKSを使ってKubernetes上に作ってきたRailsアプリケーションをデプロイしてみましょう。
システム構成
Diagramsで描いてみました。(参考:Diagrams on Dockerでシステム構成図を書いてみた - Qiita)VPCの中でEKS(ノードコンポーネント)とRDSはPrivate Subnetに配置します。今回はあまり使う機会なしですが、ノードコンポーネントをPrivate Subnetにおいているのでインターネットと通信するためにPublic SubnetにNAT Gatewayをおきます。
また、Dockerイメージを管理するためにElastic Container Registry(ECR)を使います。このシステム構成を構築し、EKSでPodをデプロイしていくために、Infrastructure as CodeツールのTerraform、AWSリソースをコマンドラインで操作するためのAWS CLI、Kubernetesを操作するためのkubectlを使います。
まずは、これらのツールを使うためのコンテナを作成して、その中でシステム構成を実現していきます。システム構築、Kubernetes操作のためのコンテナを作る
これから、Railsアプリを公開するまでにやることは大まかに以下の流れです。
- Terraformでシステム構成図の通り必要なリソースを作成する
- ECRにRailsアプリのDockerイメージを登録する
- EKSにECRに登録したRailsアプリをデプロイする
今回はこの一連のデプロイ作業をするための
deploy
コンテナを作って、その中でデプロイ作業を進めていこうと思います。ディレクトリ構造を見直す
現状のホームディレクトリはRailsアプリのソースコードが置かれていますが、新たに
rails/
ディレクトリを作成し一階層したで管理するようにします。そしてrails/
ディレクトリと同じ階層にdeploy/
ディレクトリを作成し、そちらにRailsアプリ以外のデプロイに必要なファイルを作っていくことにします。|-- rails_on_docker_handson |-- app/ |-- bin/ |-- ... |-- Gemfile |-- Gemfile.lock |-- Dockerfile |-- docker-compose.yml |-- .git/ |-- .gitignore |-- ...↓
|-- rails_on_docker_handson |-- docker-compose.yml |-- .git/ |-- .gitignore |-- rails/ | |-- app/ | |-- bin/ | |-- ... | |-- Gemfile | |-- Gemfile.lock | |-- Dockerfile | |-- deploy/ |-- Dockerfile |-- k8s/ |-- terraform/まずはこのディレクトリ・ファイル作成とファイル移動をやっていきましょう。
$ mkdir -p rails/ deploy/k8s/ deploy/terraform/ $ touch deploy/Dockerfile $ mv `ls -a | egrep -v ".git|README.md|docker-compose.yml|rails|deploy"` rails少し
mv
コマンドで複雑なことしてます。「``」のコマンドの実行結果を使ってmv [実行結果] rails
が実行されます。
「``」で囲まれたコマンドは|
が間に入っていますが、これは左側の結果に対して右側のコマンドを実行する時に使います。ls -a
はカレントディレクトリのディレクトリ、ファイルを隠しファイル含めて表示するコマンドです。この結果からegrep
の-v
オプションでその後に指定した文字列にマッチしない文字列を実行結果として返しています。
ま、そんなこんなでカレントディレクトリから.git
、README.md
、docker-compose.yml
、rails
、deploy
にマッチしないディレクトリやファイルをrails
ディレクトリに移動しました。ディレクトリ構造を変更したので、
docker-compose.yml
のbuild
とvolumes
の位置を更新します。docker-compose.ymlversion: "3" services: db: image: postgres:12.1-alpine environment: - TZ=Asia/Tokyo volumes: - - ./tmp/db:/var/lib/postgresql/data + - ./rails/tmp/db:/var/lib/postgresql/data web: - build: . + build: rails/ volumes: - - .:/app + - ./rails:/app ports: - 3000:3000 depends_on: - db environment: - RAILS_SYSTEM_TESTING_SCREENSHOT=inlineこれでファイルの移動は完了です。念のため、イメージをビルドしなおしてコンテナを起動させてみるといいかもしれません。
$ docker-compose build --no-cache web $ docker-compose up -dエラーなくサイトにアクセスできていたらOKです!
$ docker-compose downデプロイ作業用のコンテナを作る
先にのべたデプロイ手順から、デプロイ作業用のコンテナは以下のことができなければなりません。
terraform
コマンドが使えるaws
コマンドが使えるkubectl
コマンドが使えるdocker
コマンドが使える(DockerイメージのビルドとECRへのpushのため)Dockerコンテナ上で
docker
コマンドを使うため、今回はDocker in Docker(dind)のDockerイメージをベースに各コマンドをインストールすることにします。deploy/DockerfileFROM docker:dind ENV HOME="/workspace" WORKDIR ${HOME} RUN apk update && \ apk upgrade && \ # Install terraform apk add --no-cache -q terraform && \ # Install aws cli apk add --no-cache -q curl unzip python3 groff && \ curl -sO https://bootstrap.pypa.io/get-pip.py && \ python3 get-pip.py && \ pip3 install awscli --upgrade && \ rm get-pip.py && \ # Install kubectl curl -s https://amazon-eks.s3-us-west-2.amazonaws.com/1.14.6/2019-08-22/bin/linux/amd64/kubectl -o kubectl && \ chmod +x ./kubectl && \ mv kubectl /usr/local/bin
terraform
はapk add
でインストールしました。
aws
とkubectl
はそれぞれの公式の手順に従ってインストールしています。
docker-compose.yml
も更新します。docker-compose.yml- version: "3" + version: "3.7" services: ... + deploy: + build: deploy/ + environment: + - AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY + - AWS_DEFAULT_REGION=ap-northeast-1 + - KUBECONFIG=/workspace/k8s/.kube/config + volumes: + - ./rails:/workspace/rails + - ./deploy/k8s:/workspace/k8s + - ./deploy/terraform:/workspace/terraform + privileged: true
privileged: true
オプションをつけることでDockerコンテナの中からローカルのDockerデーモンを使ってdocker
コマンドを操作できるようになります。
このprivileged
はdocker-composeのversionが3.4以上じゃないと使えないので、最新の3.7を指定しています。また、
AWS_ACCESS_KEY_ID
とAWS_SECRET_ACCESS_KEY
はそれぞれ$AWS_ACCESS_KEY_ID
とRAWS_SECRET_ACCESS_KEY
から取得するようにしています。$
は環境変数を表していて同じディレクトリの.env
ファイルで定義することができます。
AWS_ACCESS_KEY_ID
やAWS_SECRET_ACCESS_KEY
はセンシティブな値なので、docker-compose.yml
とは別で管理して間違ってGitで公開したりしないようにするのがオススメです。$ touch .env.envAWS_ACCESS_KEY_ID=[AWS_ACCESS_KEY_IDを記述する] AWS_SECRET_ACCESS_KEY=[AWS_SECRET_ACCESS_KEYを記述する]とりあえず
AdministratorAccess
権限を持っているIAMがあるといいです。まだ発行していない方は公式ドキュメントを参考に作成してください!それではデプロイ操作コンテナをビルドして、コマンドが使えるようになっているかチェックしておきましょう。
$ docker-compose build deploy $ docker-compose up -d deploy $ docker-compose exec deploy ash# docker -v Docker version 19.03.8, build afacb8b7f0 # terraform version Terraform v0.12.17 # aws --version aws-cli/1.18.36 Python/3.8.2 Linux/4.19.76-linuxkit botocore/1.15.36 # kubectl version Client Version: version.Info{Major:"1", Minor:"14+", GitVersion:"v1.14.7-eks-1861c5", GitCommit:"1861c597586f84f1498a9f2151c78d8a6bf47814", GitTreeState:"clean", BuildDate:"2019-09-24T22:12:08Z", GoVersion:"go1.12.9", Compiler:"gc", Platform:"linux/amd64"} The connection to the server localhost:8080 was refused - did you specify the right host or port? # exitちょっと
kubectl
が怪しい感じですが今の段階ではクラスターと接続できていないのでエラーっぽい感じの表示がなされます。コマンド自体は使えているので問題ないです。これでデプロイ作業コンテナの準備ができました。
Terraformでシステム構築する
最初に提示したシステム構成図をTerraformで実現していきます。
これにはいろいろな記事を参考にさせていただきました。
- Terraformを使ってEKSを作成してみた | Developers.IO
- SubhakarKotta/aws-eks-rds-terraform: This setup creates AWS EKS cluster using terraform
- eksctl で VPC を作るのをやめて Terraform で作るようにしました - hatappi.blog
そして何よりもTerraformの公式ドキュメント(AWS)を読みました。
ではではTerraformの定義ファイルを作っていきましょう!
VPC周りを作る
まずはVPCなどの骨格となるネットワーク構成を作っていきます。
最初に、いろいろなところで共通的に使うことになる変数を定義してみます。
$ touch deploy/terraform/variables.tfdeploy/terraform/variables.tfvariable "project" { default = "handson" } variable "num_subnets" { default = 3 } variable "eks_name" { default = "handson-eks" }このように定義しておくことで他のファイルから
var.project
といった感じで変数を呼び出すことができます。あと、
provider
を定義しておく必要がある。今回はAWS。$ touch deploy/terraform/provider.tfdeploy/terraform/provider.tfprovider "aws" { version = "~> 2.0" }では、VPC周りの定義ファイルを作成していきます。ここではVPC、Public Subnet、Private Subnet、Internet Gateway、NAT Gateway(with Elastic IP)、Route Table、 Route Table Associationを定義していきます。
$ touch deploy/terraform/vpc.tfdeploy/terraform/vpc.tfdata "aws_availability_zones" "available" { state = "available" } ############################## # VPC ############################## resource "aws_vpc" "vpc" { cidr_block = "10.0.0.0/16" tags = { Name = "${var.project}-vpc" } } ############################## # Subnet ############################## resource "aws_subnet" "public_subnet" { count = var.num_subnets vpc_id = aws_vpc.vpc.id availability_zone = data.aws_availability_zones.available.names[ count.index % var.num_subnets ] cidr_block = cidrsubnet(aws_vpc.vpc.cidr_block, 8, count.index) map_public_ip_on_launch = true tags = { Name = "${var.project}-public-subnet-${count.index+1}" "kubernetes.io/cluster/${var.eks_name}" = "shared" } } resource "aws_subnet" "private_subnet" { count = var.num_subnets vpc_id = aws_vpc.vpc.id availability_zone = data.aws_availability_zones.available.names[ count.index % var.num_subnets ] cidr_block = cidrsubnet(aws_vpc.vpc.cidr_block, 8, var.num_subnets + count.index) map_public_ip_on_launch = false tags = { Name = "${var.project}-private-subnet-${count.index+1}" "kubernetes.io/cluster/${var.eks_name}" = "shared" } } ############################## # Internet Gateway ############################## resource "aws_internet_gateway" "igw" { vpc_id = aws_vpc.vpc.id tags = { Name = "${var.project}-igw" } } ############################## # Elastic IP ############################## resource "aws_eip" "nat" { vpc = true tags = { Name = "${var.project}-nat" } } ############################## # NAT Gateway ############################## resource "aws_nat_gateway" "nat" { allocation_id = aws_eip.nat.id subnet_id = aws_subnet.public_subnet.0.id tags = { Name = "${var.project}-nat" } } ############################## # Route table ############################## resource "aws_route_table" "public_rtb" { vpc_id = aws_vpc.vpc.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.igw.id } tags = { Name = "${var.project}-public-rtb" } } resource "aws_route_table" "private_rtb" { vpc_id = aws_vpc.vpc.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_nat_gateway.nat.id } tags = { Name = "${var.project}-private-rtb" } } ############################## # Route table association ############################## resource "aws_route_table_association" "rtba_public" { count = var.num_subnets subnet_id = element(aws_subnet.public_subnet.*.id, count.index) route_table_id = aws_route_table.public_rtb.id } resource "aws_route_table_association" "rtba_private" { count = var.num_subnets subnet_id = element(aws_subnet.private_subnet.*.id, count.index) route_table_id = aws_route_table.private_rtb.id }今回は「こいつ、動くぞ!」を目的にしているので、詳細は公式ドキュメントと見比べてみてください。
1点、aws_subnet.public_subnet
とaws_subnet.private_subnet
のタグに"kubernetes.io/cluster/${var.eks_name}" = "shared"
を入れています。これはEKSの公式ユーザーガイドに記載があるのですが、EKSがターゲットのサブネットをディスカバリーするために必須のタグです。お忘れなきよう!これで環境の骨格ができましたので、次にEKSを定義してみます。
EKSを作る
EKSではマスターコンポーネントが動作するEKSクラスターとノードコンポーネントが動作するノードグループを作る必要があります。
それぞれ、Terraformのドキュメント(EKS Cluster、EKS Node Group)に従えばさほど難しくありません。$ touch deploy/terraform/eks.tfdeploy/terraform/eks.tf############################## # IAM Role for EKS Cluster ############################## resource "aws_iam_role" "eks_iam_role" { name = "eks-iam-role" assume_role_policy = <<POLICY { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "eks.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } POLICY } resource "aws_iam_role_policy_attachment" "eks-AmazonEKSClusterPolicy" { policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" role = aws_iam_role.eks_iam_role.name } resource "aws_iam_role_policy_attachment" "eks-AmazonEKSServicePolicy" { policy_arn = "arn:aws:iam::aws:policy/AmazonEKSServicePolicy" role = aws_iam_role.eks_iam_role.name } ############################## # EKS Cluster ############################## resource "aws_eks_cluster" "eks" { name = var.eks_name role_arn = aws_iam_role.eks_iam_role.arn vpc_config { subnet_ids = concat(aws_subnet.public_subnet.*.id, aws_subnet.private_subnet.*.id) } depends_on = [ aws_iam_role_policy_attachment.eks-AmazonEKSClusterPolicy, aws_iam_role_policy_attachment.eks-AmazonEKSServicePolicy ] } ############################## # IAM Role for EKS Node Group ############################## resource "aws_iam_role" "eks_node_group_iam_role" { name = "eks-node-group-iam-role" assume_role_policy = <<POLICY { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } POLICY } resource "aws_iam_role_policy_attachment" "eks_node_group_AmazonEKSWorkerNodePolicy" { policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" role = aws_iam_role.eks_node_group_iam_role.name } resource "aws_iam_role_policy_attachment" "eks_node_group_AmazonEKS_CNI_Policy" { policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" role = aws_iam_role.eks_node_group_iam_role.name } resource "aws_iam_role_policy_attachment" "eks_node_group_AmazonEC2ContainerRegistryReadOnly" { policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" role = aws_iam_role.eks_node_group_iam_role.name } ############################## # EKS Node Group ############################## resource "aws_eks_node_group" "eks_ng" { cluster_name = aws_eks_cluster.eks.name node_group_name = "eks-ng" node_role_arn = aws_iam_role.eks_node_group_iam_role.arn subnet_ids = aws_subnet.private_subnet.*.id instance_types = ["t2.small"] scaling_config { desired_size = 3 max_size = 4 min_size = 2 } depends_on = [ aws_iam_role_policy_attachment.eks_node_group_AmazonEKSWorkerNodePolicy, aws_iam_role_policy_attachment.eks_node_group_AmazonEKS_CNI_Policy, aws_iam_role_policy_attachment.eks_node_group_AmazonEC2ContainerRegistryReadOnly ] }EKSではクラスター、ノードグループそれぞれにIAM Roleを付与してあげる必要があるので少し複雑に見えるかもしれませんが、Terraformに沿って書けばさほど難しくありません。
aws_eks_node_group.eks_ng.instance_types
でノードグループのインスタンスタイプを指定してます。今回はサンプルですし小さめのt2.small
。aws_eks_node_group.eks_ng.scaling_config
ではノードの最小数、最大数を定義しています。これに合わせてEKSがスケーリングしてくれるわけです。ちょっとdesired_size
とmin_size
の関係がわかっていないのですが...基本的にdesired_size
でノードが展開されます。もしリソースが足りなくなったらmax_size
までオートスケールしてくれます。RDSを作る
次はRDSです。
$ touch deploy/terraform/rds.tfdeploy/terraform/rds.tf############################## # Security Group for RDS ############################## resource "aws_security_group" "rds" { vpc_id = aws_vpc.vpc.id ingress { protocol = "tcp" from_port = 5432 to_port = 5432 security_groups = [aws_eks_cluster.eks.vpc_config[0].cluster_security_group_id] } tags = { Name = "sg-${var.project}-rds" } } ############################## # DB Subnet for RDS ############################## resource "aws_db_subnet_group" "default" { name = "${var.project}-db-subnet-group" subnet_ids = aws_subnet.private_subnet.*.id tags = { Name = "${var.project}-db-subnet-group" } } ############################## # RDS ############################## resource "aws_db_instance" "rds" { allocated_storage = 20 db_subnet_group_name = aws_db_subnet_group.default.name engine = "postgres" engine_version = "12.2" instance_class = "db.t2.micro" username = "handson_user" password = "handson2020" port = 5432 vpc_security_group_ids = [aws_security_group.rds.id] skip_final_snapshot = true tags = { Name = "${var.project}-db" } }Security Group、DB Subnet、DB Instanceを定義しています。
Security Groupでは
aws_security_group.rds.ingress.security_groups
でEKSに設定されたセキュリティグループをIngressで許可しており、これをaws_db_instance.rds.vpc_security_groups_id
でRDSに付与しています。こうすることでEKSのノードグループの上のPodからRDSにアクセスできるようになります。そう言えばここまでPostgreSQLはversion12.1を使っていましたが、RDSでは12.2しか使えないようです...(参考:PostgreSQL on Amazon RDS - Amazon Relational Database Service)
問題はないとは思いますが、念のためテストを回しておきましょう。docker-compose.yml... db: - image: postgres:12.1-alpine + image: postgres:12.2-alpine ...$ docker-compose run web rspec Finished in 2 minutes 11.5 seconds (files took 18.92 seconds to load) 85 examples, 0 failuresOKOK。
ECRを作る
最後にECRも作成しておきます。
$ touch deploy/terraform/ecr.tfdeploy/terraform/ecr.tf############################## # ECR for Rails app ############################## resource "aws_ecr_repository" "ecr" { name = "${var.project}_app" }これはレポジトリの名前をつけてあげてるだけですね。
TerraformでAWSリソースを構築する
ここまでで定義ファイルの準備が整いましたので、TerraformでAWSリソースを作成・構築していきます。
まず、再びデプロイ作業用のコンテナに入ります。$ docker-compose exec deploy ash# cd terraform # terraform init # terraform plan # terraform applyこれだけです。
plan
でファイルから設定するべき項目をプランニングし、apply
で適用するという感じです。
apply
の時に「Do you want to perform these actions?」と聞かれますがyes
と答えましょう。少し時間がかかりますが、
Apply Complete!
となれば環境構築は完了です!RailsアプリをECRに登録する
次に、ECRにRailsアプリのDockerイメージを登録しようと思います。
EKSではproduction環境として動かしますし、RDSに接続できるように設定をできるようにしないといけません。DBの接続情報は
config/database.yml
で設定していましたね。
ということで、そのファイルでRAILS_ENV
がproduction
の場合は接続情報を環境変数から設定できるようにしてみます。rails/config/database.yml... production: <<: *default - database: app_production - username: app + host: <%= ENV['APP_DATABASE_HOST'] %> + database: <%= ENV['APP_DATABASE_DATABASE'] %> + username: <%= ENV['APP_DATABASE_USERNAME'] %> password: <%= ENV['APP_DATABASE_PASSWORD'] %>これでそれぞれの環境変数から接続情報が設定されるようになります。環境変数の指定はKubernetesのConfigMapを使ってやりますので、また後ほど。
また、Dockerfileもdevelopment環境とproduction環境では実行したいことが異なります。
例えば、development環境ではChromeブラウザがテスト用に必要ですが、production環境にはいりません。また、bundle install
でインストールしたいgemにも差があります。
このような差分を同じDockerfileでできるように、docker build
コマンドのオプションで--build-args
を使って変数を送り込むことで動作を変えることができます。
具体的には、Dockerfileを以下のように更新します。(結構大きく変わるのでコピペしてください。)(参考:DockerFileにif文(条件分岐) - Qiita)rails/DockerfileARG BUILD_MODE="dev" FROM ruby:2.6.5-alpine3.11 ARG BUILD_MODE ARG PROD_MODE="prod" ARG RUNTIME_PACKAGES="gcc \ g++ \ less \ libc-dev \ libxml2-dev \ linux-headers \ make \ nodejs \ postgresql \ postgresql-dev \ tzdata \ yarn" ARG BUILD_PACKAGES="build-base \ curl-dev" ARG CHROME_PACKAGES="chromium \ chromium-chromedriver \ dbus \ mesa-dri-swrast \ ttf-freefont \ udev \ wait4ports \ xorg-server \ xvfb \ zlib-dev" ENV HOME="/app" ENV LANG=C.UTF-8 ENV TZ=Asia/Tokyo WORKDIR $HOME RUN apk update && \ apk upgrade && \ apk add --no-cache ${RUNTIME_PACKAGES} && \ apk add --virtual build-packs --no-cache ${BUILD_PACKAGES} && \ if [ "${BUILD_MODE}" != "${PROD_MODE}" ]; then \ apk add --no-cache ${CHROME_PACKAGES}; \ fi COPY Gemfile ${HOME} COPY Gemfile.lock ${HOME} RUN if [ "${BUILD_MODE}" = "${PROD_MODE}" ]; then \ bundle install --without development test -j4; \ else \ bundle install --without production -j4; \ fi && \ apk del build-packs COPY . ${HOME} RUN if [ "${BUILD_MODE}" = "${PROD_MODE}" ]; then \ bundle exec rails assets:precompile RAILS_ENV=production; \ else \ yarn install; \ fi EXPOSE 3000 CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
BUILD_MODE
がprod
かどうかによって挙動を分けています(デフォルトはdev
)。ARG
は変数で、docker build
の時に--build-arg [ARG_NAME]=[ARG_VALUE]
で変数を外部から引き渡すこともできるやつです。なのでprod
でビルドしたいときだけ--build-arg BUILD_MODE=prod
と指定すればproduction用のビルドができるようになります。さらに、productionには不要なファイルもあります。テストシナリオとか今までのログファイルとかは不要です。これをビルドする時に無視するために
dockerigonore
ファイルを作成して不要なファイルを定義しておきます。$ touch rails/.dockerignorerails/.dockerignore.local .pki log node_modules spec tmp .rspec yarn-error.logざっとみた感じ、この辺りが今のところ不要かなー。
またここまででGitからダウンロードしたりした場合は
config/master.key
がない状態だと思います。これだとproductionでビルドできないので生成します。$ rm rails/config/credentials.yml.enc $ docker-compose run -e EDITOR="mate --wait" web rails credentials:editこうすることで
master.key
を再生成することができる。今回は活用していませんが、credentials
について詳しくはいろいろと参照してください。(参考:Rails5.2から追加された credentials.yml.enc のキホン - Qiita)これで準備が整いました。イメージをビルドしていきます。
イメージをビルドするに際して、イメージ名を決める必要があります。これはECRに作成したリポジトリ名と同一でなくてはなりません。まず、aws
コマンドを使って名前を確認しておきます。# aws ecr describe-repositoriesJSON形式で情報が表示されますが、このうち
repositoryUri
がDockerイメージに名付けるべき名前です。またタグは1.0.0
としておきます。# cd /workspace/rails # docker build -t [repositoryUri]:1.0.0 --build-arg BUILD_MODE=prod .これで
repositoryUri
の名前をつけて、production環境用にDockerイメージをビルドできました。次にECRにログインします。
# aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin [repositoryUri] Login SucceededログインができたらECRにDockerイメージを登録できるようになるので、今作成したDockerイメージをプッシュします。
# docker push [repositoryUri]:1.0.0これでイメージの登録も完了です。
あとはEKS上にこのイメージをベースとしたPodをデプロイしていきます!
EKSにデプロイする
環境変数用のConfigMapを生成する
最初にデプロイするPod(コンテナ)に渡す環境変数をConfigMapリソースに保存します。
$ mkdir deploy/k8s/config $ touch deploy/k8s/config/rails_config.yamldeploy/k8s/rails_config.yamlapiVersion: v1 kind: ConfigMap metadata: name: rails-config data: RAILS_ENV: production RAILS_SERVE_STATIC_FILES: "true" APP_DATABASE_HOST: [RDSのエンドポイント名] APP_DATABASE_DATABASE: app_production APP_DATABASE_USERNAME: handson_user APP_DATABASE_PASSWORD: handson2020ConfigMapではこんな感じでデータを保存できるんですね。
RAILS_ENV
はRailsアプリケーションをproduction
モードで起動するための環境変数です。
RAILS_SERVE_STATIC_FILES
はassets:precompile
したCSS/JSファイルをpublic
ディレクトリから提供するための環境変数。何かしら設定されていれば有効になるので、今回はtrue
の文字列を指定。(RAILS_SERVE_STATIC_FILES
はconfig/environments/production.rb
に記述があります。)rails/config/environments/production.rb... # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? ...
APP_DATABASE_*
は先ほど、config/database.yml
にて指定した環境変数です。USERNAME
、PASSWORD
は先ほどterraform/rds.tf
で定義したものです。DATABASE
はこれからrails db:create
で作成するものなので、好きな文字列で問題ありません。
HOST
は先ほどTerraformで作成したRDSのエンドポイント名を設定する必要があります。これもaws
コマンドで確認してみましょう。# aws rds describe-db-instancesまたJSON形式で情報がアウトプットされたかと思いますが、このうち
Endpoint.Address
がエンドポイント名ですので、これを設定します。ConfigMapの宣言は以上です。
Jobリソースを宣言する
Railsアプリケーションを起動させる前にDBの作成とマイグレーションファイルの適用が必要です。
そのために1度だけ起動してコマンド発行されたら落ちるJobリソースを宣言して、Podをデプロイする前に実行しようと思います。$ mkdir deploy/k8s/settings $ touch deploy/k8s/settings/set_db_job.yamldeploy/k8s/settings/set_db_job.yamlapiVersion: batch/v1 kind: Job metadata: name: rails-db-setup spec: template: metadata: name: rails-db-setup spec: containers: - name: rails-db-setup image: [RepositoryUri]:1.0.0 imagePullPolicy: Always command: ["ash"] args: ["-c", "bundle exec rails db:create && bundle exec rails db:migrate"] envFrom: - configMapRef: name: rails-config restartPolicy: Never backoffLimit: 1ファイルの書き方は公式ドキュメントなどを参考に。
spec.template.spec.containers.image
に先ほどECRにプッシュしたRailsアプリのDockerイメージを宣言しています。そのイメージを使ってbundle exec rails db:create && bundle exec rails db:migrate
を実行することでDBの作成とマイグレーションの適用を行おうとしています。
また、envForm
で先ほど宣言したConfigMap(rails-config
)から環境変数を読み取っています。そこにはRDSのエンドポイントがAPP_DATABASE_HOST
として定義されているので、RDSに対してDBの作成とマイグレーションの適用がなされることがわかりますね。
kind
にJob
を宣言しているので、このリソースはコマンド実行が終わったら自動的に停止状態になります。PodをDeploymentする
最後に実際に動作し続けるPodリソースの宣言ファイルを作ります。Podの宣言と言いましたが、KubernetesではPodリソースの数を宣言するReplicaSetリソースのデプロイ戦略を宣言するDeploymentリソースのファイルを適用することでPodをデプロイすることが一般的です。
また、外部と通信するためのServiceリソースも一緒に宣言しちゃいましょう。$ touch deploy/k8s/deployment.yamldeploy/k8s/deployment.yamlapiVersion: apps/v1 kind: Deployment metadata: name: rails-deployment labels: app: rails spec: replicas: 3 selector: matchLabels: app: rails template: metadata: labels: app: rails spec: containers: - name: rails image: [RepositoryUri]:1.0.0 imagePullPolicy: Always ports: - containerPort: 3000 envFrom: - configMapRef: name: rails-config --- apiVersion: v1 kind: Service metadata: name: rails-service spec: type: LoadBalancer selector: app: rails ports: - protocol: TCP port: 80 targetPort: 3000
---
を挟んでDeploymentリソースとServiceリソースが宣言されているのがわかりますね。Deploymentリソースの方ではJobリソースと似ていますが、
spec.template.spec.containers.image
でECRのRailsアプリイメージをベースにし、envForm
でrails-config
のConfigMapから環境変数を読み取り起動しようとしていることがわかります。
また、spec.replicas
を3
にしているので、Podは3つ起動され、起動され続けるようにKubernetesに管理してもらいます。Serviceリソースの方では、
spec.type
にLoadBalancer
を宣言しています。これによってKubernetesのノードコンポーネントで外からの通信を許可することができます。
spec.selector
でapp: rails
を定義していますが、これはDeploymentリソースのlabel
を指定しているものです。ポートは80
ポートを受け取り、3000
ポートに流していることが読み取れ、Deploymentリソースの方でcontainerPort
として3000
を開けていることも読み取れます。EKSではServiceリソースでLoadBalancerを作成すると、AWSのNLB(Network Load Balancer)が生成されます。これによって、NLBに付与されるドメインを通じてEKSで稼働しているRailsコンテナのPodにアクセスできるようになるわけです。
ここまででファイルの準備は揃いましたので、EKSに適用していきましょう。
デプロイ
デプロイするためにはまずデプロイするクラスターを指定する必要があります。
aws
コマンドを使ってこれをやってみます。
Terraformのファイルを見返していただきたいのですが、今EKSクラスターにはhandson-eks
という名前がついています。deploy/terraform/eks.tfresource "aws_eks_cluster" "eks" { name = var.eks_name ... }deploy/terraform/variables.tfvariable "eks_name" { default = "handson-eks" }デプロイするクラスターを指定するためにconfigを更新します。
# aws eks update-kubeconfig --name handson-eksこれで
docker-compose.yml
で定義したKUBECONFIG
の場所にconfigファイルが生成されているはずです。また、これでEKSクラスターを特定できるようになったので、例えば
kubectl get nodes
コマンドでノードグループのインスタンスの状態を確認することができるようになっているはずです。# kubectl get nodes NAME STATUS ROLES AGE VERSION ip-xxx-xxx-xxx-xxx.ap-northeast-1.compute.internal Ready <none> HhMMm v1.15.10-eks-bac369 ip-xxx-xxx-xxx-xxx.ap-northeast-1.compute.internal Ready <none> 3h10m v1.15.10-eks-bac369 ip-xxx-xxx-xxx-xxx.ap-northeast-1.compute.internal Ready <none> 3h10m v1.15.10-eks-bac369では、ConfigMap -> Job -> Deployment/Serviceの順番で適用していきます。
Kubernetesでは
kubectl apply -f [file_name]
でリソースを適用していくことができます。これはとても統一的でめちゃくちゃ便利です。変更があった場合も同様のコマンドでアップデートをかけることができます(一部をのぞき)。ちなみに似たような感じで
kubectl delete -f [file_name]
でそのファイルで適用していたリソース(もっと言えばそのファイル内でラベル付されてるリソース)を削除することができます。# kubectl apply -f k8s/config/rails_config.yaml configmap/rails-config created # kubectl get configmap NAME DATA AGE rails-config 6 81sConfigMapが作成されていることがわかります。もっと詳しく中身をみたい場合は
kubectl describe cm rails-config
でみれます。次はJobです。
# kubectl apply -f k8s/settings/set_db_job.yaml job.batch/rails-db-setup created # kubectl get jobs -w NAME COMPLETIONS DURATION AGE rails-db-setup 0/1 29s 30s rails-db-setup 1/1 18s 36s
-w
オプションは変化があった時に表示が更新されるモードです。Ctrl+C
で抜け出せます。
Jobもコンプリートしたことがわかりますね。最後はDeploymentとServiceです。
# kubectl apply -f k8s/deployment.yaml deployment.apps/rails-deployment created service/rails-service created # kubectl get pods -w NAME READY STATUS RESTARTS AGE rails-db-setup-lb9cs 0/1 Completed 0 4m27s rails-deployment-56b4695fdf-2jdgl 1/1 Running 0 2m17s rails-deployment-56b4695fdf-5dxsf 1/1 Running 0 2m17s rails-deployment-56b4695fdf-npf7b 1/1 Running 0 2m17s先ほどのJobもPodとして動いていたので
Status: Completed
で残っていますね。他の3つがRailsアプリのPodです。Deploymentで宣言した通り3つ起動していますね。# kubectl get service -w NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP xxx.xxx.xxx.xxx <none> 443/TCP 3h42m rails-service LoadBalancer xxx.xxx.xxx.xxx xxxxxxxxxx.amazonaws.com 80:31230/TCP 3m47s今回生成したのは
rails-service
の方ですね。EXTERNAL-IP
に書かれているドメインがNLBに付与されているドメインです。これがRailsアプリのPodに流してくれるはず。アクセスしてみましょう。ドメインが反映されるまで少し時間がかかりますが、ちょっと待てば今まで作ってきたサイトが表示されました。
独自ドメインやHTTPS化を考えると、他にもいろいろとやらねばならぬことはある(例えば)のですが、ひとまずこれでEKS+RDSでRailsアプリを公開することができました!!!!まとめ
最後の最後は少し難しめなお題、EKSでアプリをデプロイしてみように挑戦してみましたがいかがだったでしょうか?
実際にサービスとして本番環境で運用をしようとすると、いろいろと足りないところは多いのですが、ひとまず「こいつ、動くぞ!」というところには到達できたんじゃないかと思います。今回でハンズオンは終わりです。Dockerから始まり、Railsアプリ、TDD、そしてHerokuやEKSでのサービス公開。
一通りのサービス開発の流れを体験して、「あれ、意外と調べながらやったりすればできそうだな。」というような感覚を持っていただけたなら幸いです。そうです。開発は選ばれた物にしかできない魔法ではない。学びです。直接コーディングやデプロイに関わらないロールだとしても、サービス開発に携わっているならばこういったことを知っていることは確実に優位性になるでしょう!
もしさらなる興味が湧いてきたら、個人開発とかにも挑戦してみましょう!ここまでお付き合いいただきありがとうございました!
後片付け
あ、今日はAWSを使っていろいろやりました。放置しとくとちゃりんちゃりんなのでちゃんと後片付けをしておきましょう。
まず確実にDeployment、SVCは落としておきましょう。ずっと公開されっぱなしになっちゃうので。
# kubectl delete -f k8s/deployment.yaml deployment.apps "rails-deployment" deleted service "rails-service" deleted # kubectl delete -f k8s/settings/set_db_job.yaml job.batch "rails-db-setup" deleted # kubectl delete -f k8s/config/rails_config.yaml configmap "rails-config" deleted今回の環境を全部無かったことにする場合はTerraformで削除しちゃいましょう。
# cd /workspace/terraform # terraform destroy Do you really want to destroy all resources? ... Destroy complete! Resources: 31 destroyed.これまた
yes
と答えてあげれば削除が始まります。これまた結構時間がかかりますが、跡形もなく消してくれている様子がみて取れます。本日のソースコード
Other Hands-on Links
- 投稿日:2020-04-21T05:23:56+09:00
【rails】Active Storage
Active Storage
active_storage_attachments
id name record_type record_id blob_id 1 avatar Taxonomy 3 1 10 eye_catch Article 13 10 14 og_image Site 1 14 17 favicon Site 1 17
record_type
model名
name
has_one_attachedの関連付け
record_id
articlesテーブルのid
blob_id
active_storage_blobsテーブルのid例
record_type = Article
name = eye_catcharticle.rbhas_one_attached :eye_catchactive_storage_blobs
説明部分のみ抜粋
id filename content_type 1 IMG_4837.jpg image/jpeg 10 サイバスター.jpg image/jpeg 14 ジャスティス.jpg image/jpeg 17 SSLプロトコルスタック.png image/png
filename
ファイルの実体
content_type
拡張子のタイプsite.rbvalidates :og_image, attachment: { purge: true, content_type: %r{\Aimage/(png|jpeg)\Z}, maximum: 524_288_000 } validates :favicon, attachment: { purge: true, content_type: %r{\Aimage/png\Z}, maximum: 524_288_000 }許可するcontent_typeを指定できる。
attached?
avatar.attached?で特定のuserがavatarを持っているかどうかを調べられる。
Current.user.avatar.attached?variantメソッド
- Variantを使うと新しい画像サイズが欲しくなった時に、オンデマンドで画像が生成される
# 200px * 200pxの画像にリサイズされた画像を表示 images.variant(resize: '200x200').processed
processed
をつけることで、すでにそのサイズで保存されて画像があれば、変換処理は行われず、即時にURLが返される。複数枚アップロード
レコードとファイルの間に1対多の関係を設定。
各レコードには、多数の添付ファイルをアタッチできる。site.rbhas_many_attached :imagesmultiple: trueを設定
任意のview= form_with model: [:admin, @site], url: admin_site_path do |f| = f.label :images = f.file_field :images, multiple: truesimple_form_for使用時は書式が変わるため気をつける
(検証ツールでmultipleが付与されているか確認すればこの点に気づける)= simple_form_for [:admin, @site], url: admin_site_path do |f| = f.input :images, as: :file, input_html: { multiple: true }def site_params params.require(:site).permit(:name, :favicon, :og_image, main_images: []) end空の配列にimagesが入る受け入れ準備。
multiple: trueにするとインターフェースが変わるため注意。?♀️ params.require(:site).permit(:main_images)
?♂️ params.require(:site).permit(main_images: [])active_storage_attachmentsからインスタンス取得
active_storage_attachmentsの主キーがわかっていれば
ActiveStorage::Attachment.find(params[:id])
attachmentsテーブルからインスタンスを取得できる。site/edit.html.slim- if @site.og_image.attached? = image_tag @site.og_image_url(:ogp), class: 'img-responsive' = link_to '削除', admin_site_attachment_path(@site.og_image.id), class: 'btn btn-danger', method: :delete
@site.og_image.id
でog_imageのidを引数としてコントローラに渡すことでsites_controller.rbActiveStorage::Attachment.find(params[:id])そのIDを使用してAttachment全体から合致するインスタンスを取得することができる。
(渡ってきたIDはAttachmentテーブルのものでなければならない)今回
@site.og_image.id
で取ってきたIDは赤丸のID。
このIDと紐づいたblobsから情報を取得できる。
- 投稿日:2020-04-21T02:45:27+09:00
rbenvを使用したRubyのバージョン管理
この記事は、rbenvがインストールされている事を前提に書いてあります。
インストールに関しては他記事をご参照してください。目的
Rubyのバージョンを変えずに作業していると、簡単なrbenvのバージョン管理方法を忘れてしまいがちなので、すぐに思い出せるように簡潔にまとめました。
Rubyのインストール
今回はバーション2.5.7のインストールします。
他のバーションをインストールするときは「2.5.7」の部分を書き換えてください。ターミナル# インストールできるRubyのバージョンを一覧表示 $ rbenv install -l # Ruby 2.5.7をインストール $ rbenv install 2.5.7 # インストールしたRubyのコマンドを使えるようにする $ rbenv rehash # 既にインストールされているバージョンの確認 $ rbenv versionsバージョンの変更
PCの全体で使うバージョンの設定
ターミナル$ rbenv global 2.5.7カレントディレクトリだけのバーションの設定
ターミナル$ rbenv local 2.5.7不要になったバージョンの削除
ターミナル$ rbenv uninstall 2.5.7Ruby インストール後にrailsコマンドが使えないとき
Rubyのインストール後に、Railsを使おうとすると「rails: command not found」と表示されると思います。
なので、変更したバージョンにRailsをインストールしましょう。ターミナル# gem のupdate $ gem update --system # bundler のinstall $ gem install bundler # rails のinstall $ gem install railsRailsインストール部分はこちらの記事を引用させていただきました。こちらの方が詳しく書いてあるのでおすすめです。