- 投稿日:2020-02-26T20:46:31+09:00
SEしてるけど実はあんまりコード書いたことないんだよねって人に捧ぐ、Rails on Dockerハンズオン vol.9 - Sign in -
はじめに
第9回目です。
前回はサインアップ機能を作ってみましたね。今回はサインインやサインアウト機能を作ってまいります。
今後、サインインしていないと使えない機能や見れないページなども作っていきますので、アプリがユーザーのサインイン状況を知ることができてサインインしているユーザーを特定できるようにしましょう!セッション
RailsはRESTfulなWebアプリケーションフレームワークなのでステートレスなアプリです。
しかし、サインイン機能を実装するとなるとそのユーザーがサインイン状態であることをアプリが知る必要がありますし、アプリはそのユーザーがどのユーザーなのかを特定する必要があります。
このために使われるのがセッション(Session)です。Railsではセッションを管理する便利な
session
メソッドが用意されています。
今回はこのsession
メソッドを使ってサインイン状態を管理し、マイページ機能、つまりサインインしているユーザーのユーザー詳細ページに遷移する機能を作っていきましょう。
サインインしていない場合はユーザーを特定できないので、マイページには遷移させずトップページにリダイレクトをかけるようにもしていきます。このように、セッションでサインイン状態を管理できるようになれば、その状態によるページや動作の出しわけやサインインユーザーに合わせたコンテンツの出しわけが可能になります。
今回はセッションを一つのリソースと捉えて実装していきます。
つまり、サインインしたときにsessions#create
でセッションを作成したり、サインアウトしたときにSessions#destroy
でセッションを削除したりさせます。サインインページを作ろう
セッションはSessionsコントローラーで管理してきます。
アクションとしては、
new
: サインインページに遷移create
: セッション作成(サインイン処理)destroy
: セッション削除(サインアウト処理)が必要になってきます。
今回はrails g
コマンドに頼らずにディレクトリやファイルを作成していきますね。ルーティングを作成する
まずはルーティングを作成しましょう。
config/routes.rbRails.application.routes.draw do ... get '/sign_in', to: 'sessions#new', as: :sign_in post '/sign_in', to: 'sessions#create', as: :create_session delete '/sign_out', to: 'sessions#destroy', as: :sign_out endこの辺りはもう慣れてきましたよね。
コントローラーを作成する
お次はコントローラーを作ります。
# touch app/controllers/sessions_controller.rbapp/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new @user = User.new end def create end def destroy end endとりあえずルーティングと対応したアクションを記述しておきます。
new
アクション、つまりサインインページではまたUserモデルを扱います(ユーザーのサインイン情報を入力してもらうので)。
そのため、new
アクションはUsersコントローラーと同じように@user = User.new
を記述してます。サインインページを作成する
さて、
new
アクションがレンダリングするnew.html.erb
をコーディングしてきましょう。まずは、Sessionsコントローラーのビューファイルを格納するディレクトリを作成し、ビューファイルを作りましょう。
# mkdir app/views/sessions # touch app/views/sessions/new.html.erbさて、ビューファイルをコーディングしてみます。
app/views/sessions/new.html.erb<div class="container"> <h1 class="my-5">Sign in</h1> <%= form_with model: @user, url: create_session_path, local: true do |form| %> <div class="form-group"> <%= form.label :email %> <%= form.text_field :email, class: "form-control" %> </div> <div class="form-group"> <%= form.label :password %> <%= form.password_field :password, class: "form-control" %> </div> <div class="form-check"> <%= check_box_tag :visible_password, :visible, false, class: "form-check-input" %> <%= label_tag :visible_password, "パスワードを表示する" %> </div> <div class="form-group mt-5"> <%= form.submit "Sign in", class: "btn btn-primary form-control" %> </div> <% end %> <p class="text-center">登録がまだの方は<%= link_to "こちら", sign_up_path %></p> </div> <%= javascript_pack_tag 'visible_password' %>基本的には前回のサインアップページと同じですね。今回はサインインなので
password
を入力項目に指定しました。
また、password
はサインアップページと同様、チェックボックスで表示非表示を変更できるようにしています。『Sign in』ボタンの下にまだサインアップが終わっていないユーザー向けにサインアップページへのリンクを追加しています。
<%= link_to "こちら", sign_up_path %>
だけで適切なa
タグを作成してくれるのはやはり便利ですね。一度
http://localhost:3000/sign_in
にアクセスしておきましょう。以下のようなページが表示されたら、ここまでのコーディングは成功です!
サインインページへのリンクを作る
サインインページの形が出来上がってきたので、ここでサインインページへのリンクを以下のページにつけていこうと思います。
- ヘッダーの「Sign in」リンク
- サインアップページの下部に「登録済みの方はこちら」リンクを設置
ヘッダーの「Sign in」リンク
ヘッダーの「Sign in」リンクはまだ遷移先を指定できていませんでしたね。
やっとサインインページができあがってきたので遷移先としてsign_in_path
を指定しておきます。app/views/layouts/application.html.erb... <li class="nav-item"><%= link_to "Sign in", sign_in_path, class: "nav-link" %></li> ...コーディングが終わったら、一度トップページに遷移してヘッダーの「Sign in」リンクを選択してみましょう。
サインインページに遷移できたら成功です!サインアップページの下部に「登録済みの方はこちら」リンクを設置
サインインページで「登録がまだの方はこちら」リンクを設置したように、サインアップページにも「登録済みの方はこちら」リンクを設置してみましょう。
app/views/users/new.html.erb... <div class="form-group mt-5"> <%= form.submit "Sign up!", class: "form-control btn btn-primary" %> </div> <% end %> <p class="text-center">登録済みの方は<%= link_to "こちら", sign_in_path %></p> ...こちらもサインアップページで「こちら」をクリックしてサインインページに遷移できれば成功です!
サインイン処理を作成する
ここまででサインインページの形ができあがりました。
次はサインイン処理(create
)をつくっていきます!サインイン処理ではサインインページで入力された
password
からユーザーを検索します。
ユーザーがヒットすればそのユーザーのユーザー詳細ページ(users#show
)、ユーザーが存在しない、またはパスワードが誤っているような場合はエラーメッセージが表示されるようにしましょう。少しおさらいですが、モデルを検索する場合は
find_by
メソッドを使うことで指定した属性に対して検索をすることができました。
さらに、has_secure_password
をもつモデルはauthenticate
メソッドを使ってパスワード認証ができることも思い出しましょう。
この2つを組み合わせることでユーザーの検索し認証することができそうですね。では、
create
アクションを記述していきます。app/controllers/sessions_controller.rb... def create @user = User.new(email: params[:user][:email]) user = User.find_by(email: @user.email.downcase) if user && user.authenticate(params[:user][:password]) flash[:success] = "サインインしました。" redirect_to user else flash.now[:danger] = "#{User.human_attribute_name(:email)}または#{User.human_attribute_name(:password)}をもう一度確認してください。" render :new end end ...少し複雑に見えるかもしれません。1行ずつ解説していきますね。
@user = User.new(email: params[:user][:email])まず、
@user
に新しくUserモデルを代入しています。属性値はparams[:user][:email]
を持っています。
params
はフォームからのリクエストの値を取得するメソッドで、フォームのinput
タグでname="user[email]"
と定義されている値は[:user][:email]
から取得することができます。
@user
にparams[:user][:email]
の属性値をもつUserモデルを代入しているのは、この処理がエラー(ユーザーが見つからない or パスワード認証が通らない)の場合に今入力した
これがないと入力したuser = User.find_by(email: @user.email.downcase)先ほど
@user
に設定した
Userモデルを作成するときにモデル側で
今DBには必ず全て小文字のメールアドレスが保存されているので、find_by
でdowncase
で検索文字列を小文字化して検索しています。
find_by
は検索対象が存在していた場合はモデルオブジェクトを返却し、存在しない場合はnil
を返却するメソッドであることも改めて意識しましょう。if user && user.authenticate(params[:user][:password]) # trueの処理 else # falseの処理 end次に条件分岐を設けています。
&&
は「アンド条件」、「かつ」を意味していますので、
user
user.authenticate(params[:user][:password])
の両方を満たした場合は
true
、どちらか一方でも満たさない場合はfalse
の処理に分岐します。まず
user
の条件式をみてみます。
これはuser
がnil
やfalse
でないかどうかを検証しています。
先ほどfind_by
は検索結果があればモデルオブジェクト、なければnil
を返却するといいました。
なのでこのuser
の条件はfind_by
の結果そのfalse
の処理を実行するようにするための条件ということになります。次に
user.authenticate(params[:user][:password])
の条件式をみてみます。
これは順番的にuser
の条件式を満たした場合に検証される条件式です。なのでuser
にはUserモデルのモデルオブジェクトが入っています。
authenticate
メソッドはhas_secure_password
の便利機能の一つで、平文のpassword
を与えるとハッシュ化してDBに格納しているpassword_digest
と照会し、同一であればモデルオブジェクトを、そうでなければfalse
を返却してくれるものでした。以上より、このif文の条件式の条件分岐は以下の通りになります。
入力した パスワードが 実行する処理 いない - falseの処理 いる 誤っている falseの処理 いる 正しい trueの処理 やりたいことに合致してますね。
ではtrue
の処理、false
の処理をそれぞれみてみましょう。flash[:success] = "サインインしました。" redirect_to user
true
の処理はとても単純です。
検索し認証したユーザーのユーザー詳細ページにリダイレクトさせているだけです。flashでサインインメッセージも定義してますね。flash.now[:danger] = "#{User.human_attribute_name(:email)}または#{User.human_attribute_name(:password)}をもう一度確認してください。" render :new
false
の処理も最終的にはrender :new
で再度サインインページを表示させていることがわかります。
その前にflash.now
メソッドを使ってエラー文を用意しています。
前回のUsersコントローラーのcreate
アクションでもflash
メソッドを使ってサインアップ成功時のメッセージをユーザー詳細ページに一時的に表示させることをしました。
flash.now
はそれのrender
時に利用する版と思ってください。key
をdanger
にしているのは前回と同じでBootstrapの命名規則に則っています。
flash.now
のメッセージの中身が若干複雑ですね。
まず、Rubyでは文字列の中に変数を入れる場合#{変数}
と記述することができます。
"aaa" + 変数 + "bbb"
のような書き方もできますが、"aaa#{変数}bbb"
の方がよりスマートな気がしますよね。
その変数はUser.human_attribute_name(属性名)
となっています。
human_attribute_name
メソッドは引数の属性のi18nで定義した文字列を返してくれます。ちょうどform.label
と同じような感じですね。
なので今回の場合、User.human_attribute_name(:email)
は『メールアドレス』、User.human_attribute_name(:password)
は『パスワード』が変数の結果として当て込まれます。ここまででサインイン処理(
create
アクション)をみてきました。
最後にflash.now
のメッセージを表示するコードが今のsessions/new.html.erb
にはないので、前回のusers/new.html.erb
に倣って記述します。app/views/sessions/new.html.erb... <h1 class="my-5">Sign in</h1> <% flash.each do |msg_type, msg| %> <div class="alert alert-<%= msg_type %>"><%= msg %></div> <% end %> <%= form_with model: @user, url: create_session_path, local: true do |form| %> ...動作確認
では、サインイン処理を動作確認してみましょう。
前回DBをリセットしてますので、もう一度John Smith
さんをDBに登録しておきます。> User.create(name: "John Smith", email: "john@sample.com", password: "password")
http://localhost:3000/sign_in
にアクセスして色々とチェックしてみましょう!エラー系
password
を入力していない
何も入力していない、またはどちらかだけでも入力していない場合はfind_by
かauthenticate
のいずれかが必ず失敗するのでエラーメッセージが表示されていますね。入力した
test@test.com
みたいな適当な
エラーメッセージが表示されているし、test@test.com
が残っているのも確認できましたね。
password
の組み合わせが異なっている
john@sample.com
と適当なパスワードjohn1234
を入力して確認してみましょう。
こちらもエラーメッセージが表示されていますね。john@sample.com
も残っています。正常系
期待動作は
john@sample.com
&password
でユーザー詳細ページに遷移することです。
やってみましょう!
ちゃんと期待通りの動作になりましたね。セッション管理する
ここまででパスワード認証のロジックができあがりましたね。
ただ今のままでは、「認証」自体はできましたが「認証済み」という状態を管理することはできてません。
「認証済み」という状態を管理するためにセッション管理をする機能を作っていきます。ちなみにRailsの
session
メソッドでは、半永続的で暗号化された一時Cookieが払い出されます。
これは仮にこのCookieが盗まれたとしても、それを使ってサインインを乗っ取ることはできないようになっています。
その代わりと言ってはなんですが、一度ブラウザを閉じると自動的に消える仕組みになっています。SessionsHelperを作成する
セッション管理するために
sign_in
メソッドやsign_out
メソッドなどを自前で実装していきます。
これらはコントローラーの元になるapplication_controller.rb
にコーディングしていきたいところですが、それではapplication_controller.rb
に多くのメソッドを記述する必要がでてくるため可読性が確保できなくなる恐れがあります。今回は、新たにSessionsHelperを作成し、その中で
sign_in
メソッド、sign_out
メソッドを定義します。
このSessionsHelperをApplicationControllerが読み込んでコントローラー内でメソッドを利用できるようにすることで、可読性を損なうことなく機能を実装していきましょう。まずSessionsHelperファイルを作成します。
# touch app/helpers/sessions_helper.rbapp/helpers/sessions_helper.rbmodule SessionsHelper endヘルパーは
module [helper_name]
で定義します。
最後に、ヘルパーをApplicationControllerから読み取るように定義しましょう。app/controllers/application_controller.rbclass ApplicationController < ActionController::Base protect_from_forgery with: :exception include SessionsHelper endSessionsHelperで
sign_in
メソッドを作成する次に、SessionsHelperを編集して、
sign_in
メソッドを作成していきます。
Railsではsession
メソッドが用意されており、簡単にセッションを管理することができます。app/helpers/sessions_helper.rbmodule SessionsHelper def sign_in(user) session[:user_id] = user.id end endこれだけで、
sign_in
メソッドを使ってuser.id
をセッションに記録することができます。サインインページでサインインが成功した場合に
sign_in
メソッドを呼び出してセッションを記録するようにしてみます。app/controllers/sessions_controller.rb... def create ... if user && user.authenticate(params[:user][:password]) flash[:success] = "サインインしました。" sign_in user redirect_to user else ... end ...
redirect_to
の前にsign_in user
を追加しただけです。
SessionsControllerはApplicationControllerを継承しているのでApplicationControllerで読み込んでいるSessionsHelperを利用することができます。
current_user
メソッドを作成するここまででサインインしたときにセッション情報としてサインインユーザーの
id
をCookieに格納できるようになりました。
今度はこれを使って、サインイン中のユーザー情報をアプリが特定するためのcurrent_user
メソッドを作成しましょう。
このcurrent_user
メソッドを使うことで、例えば<%= current_user.name %>
でサインイン中のユーザーの名前を表示できるようにしたりします。今、セッションに
user_id
のキーに対してuser.id
を格納しています。これはsession[:user_id]
で取り出すことができます。
このことからUser.find(session[:user_id])
またはUser.find_by(id: session[:user_id])
でサインイン中のユーザーを特定できることを思いつきますね。
ただ、find
メソッドはNotFoundの場合に例外が発生します。find_by
メソッドの場合はNotFoundでもnil
を返却するだけなのでif文などで簡単にエラー時の処理を実装できるのですが、例外の場合はちょっとクセがありますね。
今回はfind_by
メソッドで実装するようにしましょう。では、SessionsHelperに定義します。
app/helpers/sessions_helper.rbmodule SessionsHelper ... def current_user @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id] end ... end少しつまずきそうなコードですね。ちょっと紐解きます。
まずわかりやすいところから、一番後ろに
if session[:user_id]
と記入しています。
これはこれよりも前の文章が実行される条件式になっています。session[:user_id]
が存在する、つまりサインイン済みの状態であればこれよりも前の文章が実行されるというわけです。
current_user
メソッドではこの1行以外にコードはないので、session[:user_id]
が存在しない場合は何も実行されず、nil
が返却されるようになります。次に見慣れない
||=
をみてみてましょう。
これは見慣れないと思いますが、少しプログラミングをかじったことがある人であれば何かの言語で+=
のような表現を見たことがあるのではないでしょうか?
Rubyでも同じような挙動をとりますが、a += b
はa = a + b
と同義になりますね。
||
はORを表す演算子ですので、a ||= b
はa = a || b
と同義になります。
current_user
は上で少しお話したように、<%= current_user.name %>
、<%= current_user.email %>
のように使われるケースを考えています。
これが同じビューファイルで2回呼び出された場合、User.find_by(id: session[:user_id])
とだけコーディングしていた場合は2回ともDBから情報を取得しなくてはならなくなります。
そこで@current_user ||= User.find_by(id: session[:user_id])
としておくことで、一度目に<%= current_user.name %>
時に@current_user
がセットされるため二度目の<%= current_user.email %>
の場合はDBにアクセスすることなく前回の検索時のモデルオブジェクトを再利用することができるようになります。
DBの負荷軽減やDBアクセスの回数削減によるレスポンス向上を目的として、こういった記述にしてみました。まとめると、
current_user
メソッドは呼び出されたときに以下の動作をします。
サインイン ユーザー 処理 未 - nil
済 NotFound nil
済 Found サインイン済みユーザーのモデルオブジェクト 書き方を変えると以下と同じですね。
def current_user if session[:user_id] if @current_user @current_user else User.find_by(id: session[:user_id]) end end endKStep取れそうですね。笑
上の書き方の方がシンプルで可読性高いですよね。サインイン状態を確認する
signed_in?
メソッドを作るどんどんいきましょう。
次にサインイン状態をtrue
、false
で返却するsigned_in?
メソッドを作ってみます。
このメソッドによってユーザーのサインイン状態に合わせた処理を簡単に実装することができます。先ほどの
current_user
メソッドはユーザーがサインインしている場合はそのユーザーのモデルオブジェクト、サインインしていない場合はnil
を返却するメソッドでした。
そしてRailsにはnil
の場合true
を、そうでない場合false
を返却するnil?
メソッドが用意されています。
さらに!
はtrue
とfalse
が入れ替わる否定演算子です。これらを組み合わせれば
signed_in?
メソッドが作れそうですね。app/helpers/sessions_helper.rbmodule SessionsHelper ... def signed_in? !current_user.nil? end ... endサインアウトする
sign_out
メソッドを作成する最後にサインアウトするメソッドとして
sign_out
メソッドを作ってみましょう!app/helpers/sessions_helper.rbmodule SessionsHelper ... def sign_out session.delete(:user_id) @current_user = nil end ... endなんとなーくわかるとおもいますが、
session.delete(:user_id)
でCookieに保存していたsession[:user_id]
を削除してます。
さらにサインイン中は@current_user
にサインインしているユーザーのモデルオブジェクトが格納されているのでこれもnil
に初期化しています。さてさて、ここまででサインインに関して最低限必要な全てのメソッドを作成できました。
サインインの状態に合わせてヘッダーを出しわけしてみましょう!ヘッダーを更新する
ヘッダーを以下の条件で出しわけしてみます。
- 未サインイン
Home
: トップページに遷移するSign in
: サインインページに遷移する- サインイン済
Profile
: サインインしたユーザーのユーザー詳細ページに遷移するSign out
: サインアウトしてトップページに遷移するまずは、ヘッダーを
signed_in?
メソッドを使って出しわけしてみます。app/views/layouts/application.html.erb... <ul class="navbar-nav"> <% if signed_in? %> <%# サインイン済みの場合のリンク %> <li class="nav-item"><%= link_to "Profile", current_user, class: "nav-link" %></li> <li class="nav-item"><%= link_to "Sign out", sign_out_path, method: :delete, class: "nav-link" %></li> <% else %> <%# 未サインインの場合のリンク %> <li class="nav-item"><%= link_to "Home", root_path, class: "nav-link" %></li> <li class="nav-item"><%= link_to "Sign in", sign_in_path, class: "nav-link" %></li> <% end %> </ul> ...今までのヘッダーは未サインインの場合のリンクになっていたので、
signed_in?
がfalse
の場合の方に記述してます。サインイン済みの場合は新たに「Profile」と「Sign out」のリンクを作っています。
<li class="nav-item"><%= link_to "Profile", current_user, class: "nav-link" %></li>「Profile」リンクの方は今までとほぼほぼ変わらない書き方ですね。少し違う点としては
root_path
、sign_in_path
のように遷移先としてpath
を指定するのではなくcurrent_user
というモデルオブジェクトを指定している点です。
link_to
メソッドでは、遷移先としてモデルオブジェクトが指定された場合、そのモデルオブジェクトの参照パスに読み替えてくれます。つまり、user_path(current_path)
と同義になります。<li class="nav-item"><%= link_to "Sign out", sign_out_path, method: :delete, class: "nav-link" %></li>「Sign out」リンクの方は
method: :delete
の箇所が今までとは異なります。
通常link_to
メソッドはGET
メソッドでリンク先にリクエストしますが、method: [method]
で指定することで別のメソッドでリクエストすることができます。
sign_out_path
はroutes.rb
でdelete
メソッドでルーティングするように定義していましたので、この形でlink_to
のHTTPメソッドを指定してあげないとルーティングされなくなってしまいます。そういえば、
sign_out_path
でルーティングされるsessions#destroy
について、コーディングをしていませんでした。
セッションを削除するsign_out
メソッドを実行し、トップページにリダイレクトするようにコーディングしましょう。app/controllers/sessions_controller.rb... def destroy sign_out redirect_to root_path end ...ここまでで、サインイン機能、サインアウト機能、ヘッダーの出しわけについてコーディングが完了しました。
ここで一度動作確認をしていきましょう。サインイン機能、サインアウト機能、ヘッダー出しわけの動作確認
サインインしていない場合
まずサインインしていない状態で
http://localhost:3000
にアクセスしてみましょう。
すでにサインインしてしまっている場合は、ブラウザを閉じるか、「Sign out」リンクを押すかしてサインアウトしましょう。この状態ではヘッダーは「Home」リンクと「Sign in」リンクが表示されています。
「Home」リンクを選択するとトップページへ、「Sign in」リンクを選択するとサインインページに遷移することも確認できますね。
サインインしている場合
では、サインインページから
john@sample.com
でサインインしてみましょう。
すると、john@sample.com
のユーザー詳細ページに遷移し、ヘッダーも「Profile」リンクと「Sign out」リンクに変わっていることが確認できます。
また、「Profile」リンクを選択することで
john@sample.com
のユーザーのユーザー詳細ページに遷移できることも確認できますね。サインアウトを試してみる
最後にサインアウトが正しく動作するか確認してみましょう。
サインインしている状態で、「Sign out」リンクをクリックしてみてください。トップページに遷移して、ヘッダーが未サインイン状態の場合のヘッダーに戻っていることが確認できるはずです。
これで、想定どおりに動作していることが確認できましたね。
サインアップした時もサインイン状態になるようにする
今のままではサインアップした後にサインインをしないといけなくなり面倒です。
サインアップ時(users#create
)もサインイン状態になるように更新します。app/controllers/users_controller.rb... def create @user = User.new(user_params) if @user.save sign_in @user flash[:success] = "サインアップありがとう!" redirect_to @user else render :new end end ...
if @user.save
の下にsign_in @user
を追加しました。
これでDB保存が成功した場合に、そのユーザーでサインイン状態になるようになりました。では、新たにユーザーをサインアップページで作成して、サインイン状態でプロフィールページに遷移することを確認してみましょう。
プロフィールページに遷移してますし、ヘッダーのリンクからサインイン後の状態になっていることがわかりますね。サインイン状態のときに遷移できないページを作る
また、サインインしたあとにサインアップページやサインインページに遷移することは必要ありませんね。(むしろ禁止したい。)
さらに、サインイン後はトップページにもアクセスする必要はなく、ルートパスにアクセスしようとした場合、プロフィールページ(サインインユーザーのユーザー詳細ページ)に遷移させるようにしたいかもしれません。これらを実装してみましょう。
ApplicationControllerにサインインしている場合、強制的にプロフィールページに遷移させるメソッドを作成します。
app/controllers/application_controller.rb... def redirect_to_profile_if_signed_in redirect_to current_user if signed_in? end ...これを、トップページ(
static_pages#home
)、サインアップページ(users#new
,users#create
)、サインインページ(sessions#new
,sessions#create
)にルーティングされた直後(アクションが処理される前)に実行されるようにします。
これはbefore_action
メソッドを使えば容易です。app/controllers/static_pages_controller.rbclass StaticPagesController < ApplicationController before_action :redirect_to_profile_if_signed_in def home end endapp/controllers/users_controller.rbclass UsersController < ApplicationController before_action :redirect_to_profile_if_signed_in, only: [:new, :create] ... endapp/controllers/sessions_controller.rbclass SessionsController < ApplicationController before_action :redirect_to_profile_if_signed_in, only: [:new, :create] ... endStaticPagesControllerは
home
アクションしかなかったので、コントローラー全体にbefore_action
を適用しました。
UsersControllerとSessionsControllerはonly
オプションをつけて対象のアクションの実行前にのみredirect_to_profile_if_signed_in
メソッドが呼び出されるようにしました。サインイン状態にして、「トップページ」「サインアップページ」「サインインページ」にそれぞれダイレクトアクセス(URL直打ち)してみてください。
全てプロフィールページにリダイレクトされます。また、念のためサインアウトした場合は「トップページ」「サインアップページ」「サインインページ」にそれぞれアクセスできることも確認しておきましょう!
確認できましたね?
では、今日はここまでにしておきましょう!後片付け
いつものように、次回に向けてデータを消します。
$ docker-compose down $ docker-compose run --rm web rails db:migrate:resetDBコンテナが立ち上がった状態だと思うのでdownさせます。
$ docker-compose downまとめ
今回はセッションを利用してユーザーのサインイン機能、サインアウト機能を作ってみました。
さらにヘッダーの出しわけや、特定のページアクセス時にサインインしている状態だとプロフィールページにリダイレクトされる機能を作ってみました。
すでにお気づきとは思いますが、今回作成したredirect_to_profile_if_signed_in
メソッドと逆のことをすればサインインしていないと遷移できないページを作り出すことも可能ですし、条件をsigned_in?
ではないものにすれば、特定の条件のユーザー(例えばadmin
フラグを持っているユーザーとか)しかアクセスできないページを作り出すことも可能です。それにしても、色々と動作確認することも増えてきましたね...
このまま毎回手で目で確認していくのもしんどそうです。次回はこれを解決するためにTDD/BDD、そしてテスト自動化に取り組んでみます!
では、次回も乞うご期待!ここまでお読みいただきありがとうございました!
Reference
- Ruby on Rails チュートリアル:実例を使って Rails を学ぼう
- Ruby on Railsのparamsメソッドの使い方を現役エンジニアが解説【初心者向け】 | TechAcademyマガジン
- Ruby 当たり前のtrue/false - Qiita
- [WIP/初学者]flashとflash.nowの使い分け - Qiita
Links
・ Vol.1 - Introduction -
・ Vol.2 - Hello, Rails on Docker -
・ Vol.3 - Scaffold, RESTful, MVC -
・ Vol.4 - Static pages -
・ Vol.5 - Model and CRUD -
・ Vol.6 - Model validation -
・ Vol.7 - Secure password -
・ Vol.8 - Sign up -
・ Vol.9 - Sign in - ?この記事
- 投稿日:2020-02-26T20:24:46+09:00
阿佐志保『プログラマのためのDocker教科書 第2版』の誤植情報を集める
阿佐志保『プログラマのためのDocker教科書 第2版』(翔泳社, 2018)
- 出版社サイト: https://www.shoeisha.co.jp/book/detail/9784798153223
- GitHub: https://github.com/asashiho/dockertext2
出版社サイトの正誤表を見ると2件しか載っていなかったのですが、私が見つけただけでも10件ほどありましたのでまとめてみます。
まだ読み終えていないので読み終えたら問い合わせフォームからお知らせしようと思っています。
他にも見つけた方はコメントか編集リクエストで教えてください。
ページ 誤 正 27 DBMS(省略)呼ばれることもあります DBMS(省略)と呼ばれることもあります 29 需要な業務であるインフラ構成管理 重要な業務であるインフラ構成管理 48 バージョン管理手間がかかる バージョン管理の手間がかかる 88 これは、コンテナIDを呼ばれるもので これは、コンテナID と呼ばれるもので 93 「Offical」 「Official」 111 defaultというホスト名のサーバを操作していている defaultというホスト名のサーバを操作している 136 一番下の表がおかしい。false という余計な文字が入っている 140 リスト4.3がおかしい。/tmp/etc にコピーしていない 140 nginx.cong nginx.conf 144 1つのみなります 1つのみとなります 156 Docker image ls コマンド docker image ls コマンド 188 リスト5.60がおかしい。host.html は /docker_dir/web の下にあるはず あと、誤植ではないのですが、6章の GitHub と Docker Hub の連携で使用する Dockerfile のビルドが失敗します。誰か助けて!
Enjoy!
- 投稿日:2020-02-26T18:17:59+09:00
Docker Compose でホストとクライアントの両方にて Rails を起動したいそこのあなた!
なにこれ?
あんまりいないとは思うけど、Rails で Docker Compose を使用しているときにホスト側とクライアント側で Rails server を起動したいときはありませんか?自分の場合だと、System Test がどうしても Docker 環境のみで動作させる方法が見つけ出せなかったので、自分の場合 Rails を Ubuntu 環境でシステムテストを走らせて、それ以外は Docker 内のコンテナで動作させたいと思ったのでこういう環境が必要になった。そんなことをしようとしていると困るのが、(Rails のルート)/tmp/ に作られるファイルがホスト側とクライアント側で競合して、同時に起動できないという問題だった。そんなときに読んで解決できる糸口となったのが「DockerでVolumeをマウントするとき一部を除外する方法」だったので、自分もココにその軌跡をのこしておくことにする。
なにするの?
要は Rails 同時に起動できない原因は tmp フォルダが存在するからである。こいつさえなければ、ポートの競合とかはともかく、起動できるのだ。要はお互いが疎な関係になればいい。つまり、クライアントとホストの tmp フォルダが同期しなければいい。以下のコードは tmp フォルダだけはホストと動悸しないようにしてある。
version: '3' volumes: rails_tmp_data: services: as: build: context: ./ ports: - 3333:3000 volumes: - ./:/usr/src/app/ - rails_tmp_data:/usr/src/app/tmp/ command: /bin/sh -c "yarn install --check-files && bundle install && rails db:create && rails db:migrate && rm -f /usr/src/app/tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0' --early-hints"なお上記のコードは、要点だけに絞っている。そのため、コピペでは動かないのは、予めご了承いただきたい。
これでおしまい
ポートの設定を端折ってしまった。そこらへんは、ココでは記事にしない。途中で記事を書くのがめんどくさくなったからね。ググるかなにかしてください。そんじゃねー。
タイトルについて
スタパ斉藤マジリスペクト、というわけでなく、冗談抜きでこんなタイトルしか思いつかなかった。
- 投稿日:2020-02-26T17:57:36+09:00
あまり勧められない Docker-in-Docker のススメ
Docker in Docker とは?
Docker in Docker とは?
- Docker コンテナ内に Docker 処理系そのものを収容
- 内側の Docker の中にコンテナを立ち上げると… →外から見ると入れ子状になる
Docker in Docker の動作イメージ
ミニマムな構成
オフィシャルのイメージがあるのでそれを使う (タグの後ろに
-dind
と付いている)docker-compose.yaml--- version: "3.7" services: docker: image: docker:19.03-dind privileged: trueヤバいです、主にセキュリティー的な意味で。
privileged: true(ヤバすぎるので抜粋再掲)
詳しい説明は省略しますが、 Linux 内に隔離された空間を作るには相当の特権が必要なようです。
おしまい
んなわけあるか!!
色々いじってみる
ミニマム構成の問題点
- あんまりメリットが見えない
内側の Docker の通信路が TCP
→ docker build の時にコンテキストといって色々ファイルを Docker のデーモンに送りつけているのだが、その通信路と考えるといささか心もとない
メリットを出そう
隣接するコンテナからの指示で、内側の Docker にコンテナを建てられると便利そう。
docker-compose.yaml--- version: "3.7" services: docker: image: docker:19.03-dind privileged: true frontend: image: docker:latest depends_on: - docker
通信路の改善 ~よし、ならば UNIX ドメインソケットだ~
/var/run/docker.sock
を隣接するコンテナと共有できれば… そういう時はvolumes
を使いましょう。docker-compose.yaml--- version: "3.7" services: docker: image: docker:19.03-dind privileged: true volumes: - run:/run:rw # /var/run は /run へのシンボリックリンクなので… 以下同様 environment: - DOCKER_TLS_CERTDIR= frontend: image: docker:latest volumes: - run:/run:rw depends_on: - docker environment: - DOCKER_HOST=unix:///var/run/docker.sock volumes: run: # tmpfs-as-a-volume: https://stackoverflow.com/a/45759927 driver_opts: type: tmpfs device: tmpfs o: size=1M,mode=0755,uid=0,gid=0
- 投稿日:2020-02-26T10:25:16+09:00
dockerのポートフォワードをIPv6にも対応させる
概要
squidでActiveDirectory連携とSSLインターセプトするProxyをdockerで手軽につくる
Docker Composeでネットワークサービス群を5分で作れるようにした(dhcp/radius/proxy/tftp/syslog/dns)
上記のエントリでつくったProxyにULAのIPv6 Onlyな環境からアクセスしようとしたら、できなかった。以下、Windowsの状況ですが、iOSとAndroidも混在しているので、DHCPv6が使えず(AndroidがDHCPv6つかえない)、ゲートウェイでRAとRDNSS(ドメインサフィックスも付与)を動作させている状態です。
Wireless LAN adapter Wi-Fi: 接続固有の DNS サフィックス . . . . .: prosper2.net 説明. . . . . . . . . . . . . . . . .: Intel(R) WiFi Link 5300 AGN 物理アドレス. . . . . . . . . . . . .: 00-21-6A-CA-4A-A2 DHCP 有効 . . . . . . . . . . . . . .: はい 自動構成有効. . . . . . . . . . . . .: はい IPv6 アドレス . . . . . . . . . . . .: fd5a:ceb9:ed8d:fe10:70ff:9d38:95b6:9682(優先) 一時 IPv6 アドレス. . . . . . . . . .: fd5a:ceb9:ed8d:fe10:4ca8:def3:2da5:a3b6(優先) リンクローカル IPv6 アドレス. . . . .: fe80::70ff:9d38:95b6:9682%13(優先) デフォルト ゲートウェイ . . . . . . .: fe80::b60c:25ff:fe0f:df11%13 DHCPv6 IAID . . . . . . . . . . . . .: 117449066 DHCPv6 クライアント DUID. . . . . . .: 00-01-00-01-24-DA-AE-0D-5C-26-0A-2D-95-59 DNS サーバー. . . . . . . . . . . . .: fd5a:ceb9:ed8d:fe0a:10:254:10:251 NetBIOS over TCP/IP . . . . . . . . .: 無効 接続固有の DNS サフィックス検索の一覧: prosper2.netなぜ通信できなかったのか
Dockerネットワークは以下のコマンドで作成したものを利用していました。
# docker network create --ipv6 --driver=bridge --subnet=fd5a:ceb9:ed8d:dead::/64 br_infra_netIPv4のDNATは設定されていた。(ほかのコンテナのものも入ってます)
# nft list chain ip nat PREROUTING ; nft list chain ip nat DOCKER table ip nat { chain PREROUTING { type nat hook prerouting priority -100; policy accept; fib daddr type local counter packets 42 bytes 6049 jump DOCKER } } table ip nat { chain DOCKER { meta l4proto tcp tcp dport 8081 counter packets 2 bytes 104 dnat to 172.18.0.2:8081 meta l4proto tcp tcp dport 53 counter packets 0 bytes 0 dnat to 172.18.0.3:53 meta l4proto udp udp dport 53 counter packets 0 bytes 0 dnat to 172.18.0.3:53 meta l4proto udp udp dport 514 counter packets 10 bytes 3332 dnat to 172.18.0.4:514 meta l4proto tcp tcp dport 8080 counter packets 0 bytes 0 dnat to 172.18.0.5:8080 meta l4proto udp udp dport 1813 counter packets 0 bytes 0 dnat to 172.18.0.6:1813 meta l4proto udp udp dport 1812 counter packets 0 bytes 0 dnat to 172.18.0.6:1812 } }IPv6はされてなかった。悲しい。
# nft list chain ip6 nat PREROUTING table ip6 nat { chain PREROUTING { type nat hook prerouting priority -100; policy accept; } }どうやらコンテナにIPv6ふってるんだからNDPで直接通信してね。
みたいなニュアンスらしい。(違ってたらごめんなさい)やだ、めんどくさい。
IPv4とおんなじ考え方で通信させたいんだ。スクリプトで対応した
ちゃんとdockerのマニュアルとか読めば解決策があるような気もするのですが、面倒なのでnftでDNATすることにしました。
IPv4のnftを参考にして、IPv6もおなじようなDNATするスクリプトを作成。スクリプトは
jq
を利用しています。
dnf -y install jq
などでインストールしておく必要があります。ipv6_port_bind.pl#!/usr/bin/perl use strict; my ($buf,$addr,$cnt,$port,$prot,$handle,$cmd); my $CHAIN = "DOCKER-USER-ipv6-dnat"; if($ARGV[0] !~ /flush|^$/){ print "usage : $0 [ flush ]\n"; exit; } #################### # FLUSH RULES #################### print "---------- FLUSH exist rules and chains ----------\n"; foreach(split(/\n/,`nft list chain ip6 nat PREROUTING -a | grep "jump $CHAIN" 2> /dev/null`)){ chomp($_); $handle = (split(/\ /,$_))[-1]; $cmd = "nft delete rule ip6 nat PREROUTING handle $handle"; print $cmd."\n"; system("$cmd 2> /dev/null"); } $cmd = "nft delete chain ip6 nat $CHAIN"; print $cmd."\n"; system("$cmd 2> /dev/null"); if($ARGV[0] eq "flush"){ exit; } print "\n"; #################### # NEW RULES #################### $buf = ""; $buf .= "nft create chain ip6 nat $CHAIN\n"; foreach $cnt (split(/\n/,`docker-compose ps --service`)){ chomp($cnt); $addr = `docker inspect $cnt | jq '.[].NetworkSettings.Networks[].GlobalIPv6Address'`; chomp($addr); $addr =~ s/\"//g; foreach(split(/\n/,`docker inspect $cnt | jq '.[].HostConfig.PortBindings|keys' | egrep "tcp|udp" `)){ chomp($_); $_ =~ s/\"|\,|\ //g; $port = (split(/\//,$_))[0]; $prot = (split(/\//,$_))[1]; #print "$addr : $port $prot \n"; $buf .= "nft add rule ip6 nat $CHAIN meta l4proto $prot $prot dport $port counter dnat to [$addr]:$port\n"; } } $buf .= "nft add rule ip6 nat PREROUTING fib daddr type local counter jump $CHAIN\n"; print "---------- CREATE chain and ADD rules ----------\n"; system($buf); print $buf."\n"; print "---------- CHECK rules and chain ----------\n"; print `nft list chain ip6 nat PREROUTING ; nft list chain ip6 nat $CHAIN`; exit;内容は、IPv4のDNATの処理を同じものを、
docker-compose.yml
で生成したコンテナに対して実施しています。
docker-compose.yml
と同じディレクトリにおいて、スクリプトを実行すると、起動しているコンテナに対してdocker inspect コンテナ
を実行します。
得られた出力からポートバインドの設定を抜き出して、nft
の処理を生成しています。実際に動作させると、以下のようになります。
# ./ipv6_port_bind.pl ---------- FLUSH exist rules and chains ---------- nft delete rule ip6 nat PREROUTING handle 250 nft delete chain ip6 nat DOCKER-USER-ipv6-dnat ---------- CREATE chain and ADD rules ---------- nft create chain ip6 nat DOCKER-USER-ipv6-dnat nft add rule ip6 nat DOCKER-USER-ipv6-dnat meta l4proto udp udp dport 514 counter dnat to [fd5a:ceb9:ed8d:dead::4]:514 nft add rule ip6 nat DOCKER-USER-ipv6-dnat meta l4proto tcp tcp dport 8081 counter dnat to [fd5a:ceb9:ed8d:dead::2]:8081 nft add rule ip6 nat DOCKER-USER-ipv6-dnat meta l4proto tcp tcp dport 8080 counter dnat to [fd5a:ceb9:ed8d:dead::5]:8080 nft add rule ip6 nat DOCKER-USER-ipv6-dnat meta l4proto udp udp dport 1812 counter dnat to [fd5a:ceb9:ed8d:dead::6]:1812 nft add rule ip6 nat DOCKER-USER-ipv6-dnat meta l4proto udp udp dport 1813 counter dnat to [fd5a:ceb9:ed8d:dead::6]:1813 nft add rule ip6 nat DOCKER-USER-ipv6-dnat meta l4proto tcp tcp dport 53 counter dnat to [fd5a:ceb9:ed8d:dead::3]:53 nft add rule ip6 nat DOCKER-USER-ipv6-dnat meta l4proto udp udp dport 53 counter dnat to [fd5a:ceb9:ed8d:dead::3]:53 nft add rule ip6 nat PREROUTING fib daddr type local counter jump DOCKER-USER-ipv6-dnat ---------- CHECK rules and chain ---------- table ip6 nat { chain PREROUTING { type nat hook prerouting priority -100; policy accept; fib daddr type local counter packets 0 bytes 0 jump DOCKER-USER-ipv6-dnat } } table ip6 nat { chain DOCKER-USER-ipv6-dnat { udp dport shell counter packets 0 bytes 0 dnat to [fd5a:ceb9:ed8d:dead::4]:shell tcp dport tproxy counter packets 0 bytes 0 dnat to [fd5a:ceb9:ed8d:dead::2]:tproxy tcp dport http-alt counter packets 0 bytes 0 dnat to [fd5a:ceb9:ed8d:dead::5]:http-alt udp dport radius counter packets 0 bytes 0 dnat to [fd5a:ceb9:ed8d:dead::6]:radius udp dport radius-acct counter packets 0 bytes 0 dnat to [fd5a:ceb9:ed8d:dead::6]:radius-acct tcp dport domain counter packets 0 bytes 0 dnat to [fd5a:ceb9:ed8d:dead::3]:domain udp dport domain counter packets 0 bytes 0 dnat to [fd5a:ceb9:ed8d:dead::3]:domain } }複数回起動しても問題ないように、処理の際には、既存ルールをフラッシュしてから、再作成しています。
nft ~
で始まる行が、実際に発行されるnftコマンドです。
最後に新たに作成したルールとCHAINを表示しています。これで、ULAなIPv6 Only環境からもDockerコンテナにアクセスできるようになりました。
よかった。
- 投稿日:2020-02-26T09:35:57+09:00
Dockerのマウント3種類についてわかったことをまとめる
はじめに
Dockerのマウント3種類(
Volumes
、bind mounts
、tmpfs mounts
)について、
公式リファレンスを読んで、以下の項目をまとめたものになります。
- マウントされたデータの保存先について
Volumes
、bind mounts
、tmpfs mounts
の概要Volumes
、bind mounts
、tmpfs mounts
の使用例マウントの種類によるデータの保存先
マウントの種類によらず、コンテナ内からは同じようにデータが見えます。
コンテナのファイルシステム内のディレクトリまたは個別のファイルとして公開されます。
3種類のマウントの違いについて考える上で、ポイントとなるのはデータがDockerホストのどこにあるかを考えることです。
Volumes
は、Docker(Linuxでは/ var / lib / docker / volumes /
)によって管理されるホストファイルシステムの一部に保存されます。Dockerを使用したプロセス以外の場合では、Dockerによって管理されるファイルシステムを変更しないでください。Volumes
は、Dockerでデータを保持するための最良の方法です。
bind mounts
は、ホストシステムのどこにでも保存できます。DockerホストまたはDockerコンテナ上の非Dockerプロセスは、いつでも変更できます。
tmpfs mounts
はホストシステムのメモリにのみ保存され、ホストシステムのファイルシステムには書き込まれません。それぞれマウントタイプについて
Volumes
について
Volumes
が作成されると、Dockerホストのディレクトリに保存されます。
Volumes
がコンテナにマウントされると、このディレクトリはコンテナにマウントされます。
bind Mount
と違って、Volumes
はDockerによって管理され、ホストマシンのコア機能から分離されます。特定の
Volumes
は複数のコンテナに同時にマウントできます。実行中のコンテナがボリュームを使用していない場合でも、ボリュームは引き続き使用でき、自動的に削除されません。
docker system prune
で削除することができます。
Bind mounts
について
Bind mounts
はVolumes
と比較して機能が限られています。
Bind mounts
を使用すると、ホストマシン上のファイルやディレクトリはコンテナにマウントされます。- マウント対象のファイルやディレクトリはホストマシン上の絶対パス(
full path
)によって参照されます。
tmpfs mounts
について
tmpfs mount
はDockerホストあるいはコンテナ内のディスクのどちらにも保持されません。
コンテナが作成され削除されるまでの間に、非永続的な状態または機密情報を保存することができます。マウントの使用例
Volumes
の使用例
- 複数の実行中のコンテナ間でデータを共有したい場合。
- 複数のコンテナが読み取り専用か読み書きのいずれかで、同時にマウントできる
- Dockerホストが特定のディレクトリまたはファイル構造を持つことが保証されていない場合。
- ホストのファイルやディレクトリ等の構成を実行中のコンテナから分離することが
Volumes
にはできます。ローカルではなく、リモートホストやクラウドプロバイダーに保存する場合
あるDockerホストから別のDockerホストにデータを移行、復元、バックアップしたい場合。
Bind mounts
の使用例一般にできるだけ
Volumes
を使用すべきと公式リファレンスでは述べられています。以下のユースケースに
bind mounts
が適していると記述されています。
ホストマシンからコンテナへの構成(ファイルやディレクトリ構成)を共有したい場合。
- ホストマシンから各コンテナに
/etc/resolv.conf
をマウントすることにより、DockerがデフォルトでコンテナにDNS解決を提供することができます。ソースコードや作成したアーティファクトをDockerホストとコンテナ間で共有したい場合。
- 例えば、Mavenの
target/
ディレクトリをマウントした場合に、DockerホストでMavenプロジェクトをビルドするたびにコンテナは再構築されたアーティファクトにアクセスします。Dockerホストのファイルやディレクトリ構成が、コンテナが必要とするバインドマウントに一致することが保証されている場合。
tmpfs
マウントの使用例
tmpfs
マウントはデータをホストマシンかコンテナ内でも保持したくない場合。
- これはセキュリティ上の理由か、大量の非永続化データを書き込む必要がある場合にコンテナのパフォーマンスを保護するためです。
- 投稿日:2020-02-26T09:00:15+09:00
Dockerコンテナ内からホストへ`localhost` でアクセスしてみる
のっぴきならぬ事情でDockerコンテナ内からホストへ
localhost
でアクセスする必要なときに役立ちそうなのでメモ。Dockerコンテナ内からホストへアクセスするには
こちらの記事が参考になりました。
--add-host
オプションを利用すればなんとかなりそうです。Dockerのコンテナの中からホストOS上のプロセスと通信する方法 - Qiita
https://qiita.com/Iju/items/badde64d530e6bade382
localhost
じゃなくて良いのなら
host.docker.internal
というDNS名が用意されているので、それを利用すればよさそうです。
ドキュメントによるとMac/Windowsで利用できそうです。Networking features in Docker Desktop for Windows | Docker Documentation
https://docs.docker.com/docker-for-windows/networking/The host has a changing IP address (or none if you have no network access). From 18.03 onwards our recommendation is to connect to the special DNS name host.docker.internal, which resolves to the internal IP address used by the host. This is for development purpose and will not work in a production environment outside of Docker Desktop for Windows.
(Google翻訳)ホストのIPアドレスは変更されています(ネットワークアクセスがない場合はなし)。18.03以降では、ホストが使用する内部IPアドレスに解決される特別なDNS名host.docker.internalに接続することをお勧めします。 開発目的のためであり、Docker Desktop for Windows以外の実稼働環境では機能しません。
Networking features in Docker Desktop for Mac | Docker Documentation
https://docs.docker.com/docker-for-mac/networking/The host has a changing IP address (or none if you have no network access). From 18.03 onwards our recommendation is to connect to the special DNS name host.docker.internal, which resolves to the internal IP address used by the host. This is for development purpose and will not work in a production environment outside of Docker Desktop for Mac.
(Google翻訳)ホストのIPアドレスは変更されています(ネットワークアクセスがない場合はなし)。 18.03以降では、ホストが使用する内部IPアドレスに解決される特別なDNS名host.docker.internalに接続することをお勧めします。 これは開発用であり、Docker Desktop for Mac以外の運用環境では動作しません。
試してみる
準備
まずホストでHTTPサービスを80ポートで立ち上げます。
> python -m http.server 80 Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ... # Dockerコンテナを起動するのでもOK > docker run -it --rm -p 80:80 python \ python -m http.server 80確認
立ち上げたサービスに対してDockerコンテナ内から
curl
でアクセスしてみます。
利用するDockerイメージはなんでもよいです。curl
がインストールされていなかったら先にインストールします。> docker run -it --rm alpine \ sh -c 'apk add curl;curl http://localhost:80' (略) OK: 7 MiB in 18 packages curl: (7) Failed to connect to localhost port 80: Connection refusedはい。
コンテナ内でlocalhost
に対してアクセスすると当然ながらコンテナ内へのアクセスとなり、エラーとなります。
--add-host
を足してみる
--add-host
を利用するとコンテナ内の/etc/hosts
ファイルに設定を追加できるそうです。Dockerで/etc/hostsファイルが操作出来ない対策 - Qiita
https://qiita.com/jagaximo/items/6b71a03518bbd53d4de6> docker run -it --rm alpine \ cat /etc/hosts 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters 172.17.0.3 9f215da7195eなので、
localhost
に対してホストのIPアドレスをみにいくように設定を追加してみます。# ホストのプライベートIPアドレスを確認 > ipconfig getifaddr en0 > docker run -it --rm \ --add-host=localhost:<ホストのプライベートIPアドレス> \ alpine \ cat /etc/hosts 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters <ホストのプライベートIPアドレス> localhost 172.17.0.3 6aff7879fc0a追加できることが確認できたら
curl
でアクセスしてみます。> docker run -it --rm \ --add-host=localhost:<ホストのプライベートIPアドレス> \ alpine \ sh -c 'apk add curl;curl -D - -s -o /dev/null http://localhost:80' (略) OK: 7 MiB in 18 packages HTTP/1.0 200 OK Server: SimpleHTTP/0.6 Python/3.8.1 Date: Wed, 12 Feb 2020 02:43:26 GMT Content-type: text/html; charset=utf-8 Content-Length: 987やったぜ。
使う機会があるかはわかりませんが、できることは確認できました。参考
Dockerのコンテナの中からホストOS上のプロセスと通信する方法 - Qiita
https://qiita.com/Iju/items/badde64d530e6bade382Networking features in Docker Desktop for Windows | Docker Documentation
https://docs.docker.com/docker-for-windows/networking/Networking features in Docker Desktop for Mac | Docker Documentation
https://docs.docker.com/docker-for-mac/networking/Dockerで/etc/hostsファイルが操作出来ない対策 - Qiita
https://qiita.com/jagaximo/items/6b71a03518bbd53d4de6Pythonの標準ライブラリでさくっとAPIサーバとWebサーバを立ち上げる - Qiita
https://qiita.com/kai_kou/items/6cf5930330b85fa583b0cURLコマンドでレスポンスヘッダのみを取得する - Qiita
https://qiita.com/yousan/items/fcc15e1046939c465ab7macOS で自分のプライベート IP アドレスを見つける方法 - yu8mada
https://yu8mada.com/2018/07/14/how-to-find-my-private-ip-address-in-macos/