前記事:Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)
概要
原著者の許諾を得て、CC BY-NC-SAライセンスに基づき翻訳・公開いたします。
- 英語記事:Creating a Heroku-like Deployment Solution with Docker
- 原文公開日: 2017/06/07
- 著者:Pedro Cavalheiro
- サイト:Semaphoreci.com
Rails: DockerでHeroku的なデプロイソリューションを構築する: 後編(翻訳)
Herokuライクなデプロイソリューションの構築方法を解説します。
特定のクラウドプロバイダや、Dockerに関連しないツールを必要としません。
ここまでのまとめ
これで自動デプロイの構築に必要な材料がすべて揃いました。以下は最終的なコードであり、deployer.rbという名前でrootフォルダに保存できます。各行の動作を見てみましょう。
# deployer.rbclass Deployer APPLICATION_HOST = '54.173.63.18'.freeze HOST_USER = 'remoteuser'.freeze APPLICATION_CONTAINER = 'mydockeruser/application-container'.freeze APPLICATION_FILE = 'application.tar.gz'.freeze ALLOWED_ACTIONS = %w(deploy).freeze APPLICATION_PATH = 'blog'.freeze def initialize(action) @action = action abort('Invalid action.') unless ALLOWED_ACTIONS.include? @action end def execute! public_send(@action) end def deploy check_changed_files copy_gemfile compress_application build_application_container push_container remote_deploy end private def check_changed_files return unless `git -C #{APPLICATION_PATH} status --short | wc -l` .to_i.positive? abort('Files changed, please commit before deploying.') end def copy_gemfile system("cp #{APPLICATION_PATH}/Gemfile* .") end def compress_application system("tar -zcf #{APPLICATION_FILE} #{APPLICATION_PATH}") end def build_application_container system("docker build -t #{APPLICATION_CONTAINER}:#{current_git_rev} .") end def push_container system("docker push #{APPLICATION_CONTAINER}:#{current_git_rev}") end def remote_deploy system("#{ssh_command} docker pull "\ "#{APPLICATION_CONTAINER}:#{current_git_rev}") system("#{ssh_command} 'docker stop \$(docker ps -q)'") system("#{ssh_command} docker run "\ "--name #{deploy_user} "\ "#{APPLICATION_CONTAINER}:#{current_git_rev}") end def current_git_rev `git -C #{APPLICATION_PATH} rev-parse --short HEAD`.strip end def ssh_command "ssh #{HOST_USER}@#{APPLICATION_HOST}" end def git_user `git config user.email`.split('@').first end def deploy_user user = git_user timestamp = Time.now.utc.strftime('%d.%m.%y_%H.%M.%S') "#{user}-#{timestamp}" endendif ARGV.empty? abort("Please inform action: \n\s- deploy")endapplication = Deployer.new(ARGV[0])begin application.execute!rescue Interrupt puts "\nDeploy aborted."endそれでは1つずつ手順を追ってみましょう。
APPLICATION_HOST = '54.173.63.18'.freeze HOST_USER = 'remoteuser'.freeze APPLICATION_CONTAINER = 'mydockeruser/application-container'.freeze APPLICATION_FILE = 'application.tar.gz'.freeze ALLOWED_ACTIONS = %w(deploy).freeze APPLICATION_PATH = 'blog'.freezeここでは値の重複を避けるためにいくつかの定数をコードで定義しています。APPLICATION_HOSTは実行するサーバーのリモートIPアドレス、HOST_USERはリモートサーバーのユーザー名、APPLICATION_CONTAINERはアプリをラップするコンテナの名前です。APPLICATION_FILEは圧縮したアプリのファイル名なので名前は自由に変えられます。ALLOWED_ACTIONSは許可する操作の配列であり、どの操作を利用可能にするかを簡単に定義できます。最後のAPPLICATION_PATHはアプリへのパスです。今回の例ではblogとしています。
def initialize(action) @action = action abort('Invalid action.') unless ALLOWED_ACTIONS.include? @action end def execute! public_send(@action) end上は、(ALLOWED_ACTIONSで)利用できる各メソッドのバリデーションと呼び出しを行うラッパーです。これを用いることで、コードをリファクタリングする必要なしに、呼び出し可能な新しいメソッドを簡単に追加できます。
def deploy check_changed_files copy_gemfile compress_application build_application_container push_container remote_deploy end上はデプロイ手順です。これらのメソッドは先の例とほぼ同じですが、わずかな変更があります。それぞれの手順を見てみましょう。
def check_changed_files return unless `git -C #{APPLICATION_PATH} status --short | wc -l` .to_i.positive? abort('Files changed, please commit before deploying.') endアプリのデプロイにはローカルのコードを使っているので、ファイルが変更されているかどうかをチェックして、変更がある場合はデプロイを行わないようにするのがよい方法です。この手順ではファイルの作成や変更を検出するのにgit status --shortを使っています。-Cフラグはgitでチェックする対象(この例ではblog)を定義します。不要ならこの手順を取り除くこともできますが、おすすめしません。
def copy_gemfile system("cp #{APPLICATION_PATH}/Gemfile* .") end上は、デプロイのたびにblogのルートディレクトリにあるGemfileとGemfile.lockをコピーします。これによって、デプロイが完了する前にすべてのgemがインストールされるようになります。
def compress_application system("tar -zcf #{APPLICATION_FILE} #{APPLICATION_PATH}") endメソッド名からわかるとおり、この手順ではアプリ全体を圧縮して1つのファイルにします。このファイルは後でコンテナに含められます。
def build_application_container system("docker build -t #{APPLICATION_CONTAINER}:#{current_git_rev} .") endこのメソッドは、コンテナのビルド手順を実行します。このときに依存ライブラリやgemをすべてインストールします。Gemfileが変更されるたびにDockerでそのことが検出されてインストールが行われるので、依存ライブラリの更新を気にする必要はありません。依存ライブラリが変更されるたびに多少時間がかかります。変更が何もない場合、Dockerはキャッシュを使うので手順の実行はほぼ瞬時に完了します。
def push_container system("docker push #{APPLICATION_CONTAINER}:#{current_git_rev}") endこのメソッドは、Docker Registryに新しいコンテナをアップロードします。最新のコミットハッシュをgitで取得しているこのcurrent_git_revメソッドにご注目ください。各デプロイの識別にはこのコミットハッシュを使います。アップロードしたコンテナはすべてDockerHubコンソールで確認できます。
def remote_deploy system("#{ssh_command} docker pull "\ "#{APPLICATION_CONTAINER}:#{current_git_rev}") system("#{ssh_command} 'docker stop \$(docker ps -q)'") system("#{ssh_command} docker run "\ "--name #{deploy_user} "\ "#{APPLICATION_CONTAINER}:#{current_git_rev}") endここでは以下の3つを行っています。
docker pull: リモートサーバーにアップロードしたコンテナをpullします。ssh_commandメソッド呼び出しは、リモートコマンドの送信が必要になるたびに、コードの重複を避けるための単なるラッパーです。docker stop $(docker ps -q): 新しいコンテナを実行するときにポート番号が衝突しないようにするため、実行中のコンテナをすべて停止します。docker run: 正しいタグを与えて新しいコンテナを起動し、現在のgitユーザーとタイムスタンプに基づいて名前を付けます。これは、現在実行中のアプリをデプロイしたユーザーを知る必要がある場合に便利です。名前を確認するには、リモートサーバーでdocker psコマンドを入力します。
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES01d777ef8d9a mydockeruser/application-container:aa2da7a "/bin/sh -c 'cd /t..." 10 minutes ago Up 10 minutes 0.0.0.0:3000->3000/tcp mygituser-29.03.17_01.09.43if ARGV.empty? abort("Please inform action: \n\s- deploy")endapplication = Deployer.new(ARGV[0])begin application.execute!rescue Interrupt puts "\nDeploy aborted."end上はCLIから引数を受け取って、アプリのデプロイを実行します。Ctrl-Cでデプロイをキャンセルすると、rescueブロックでわかりやすいメッセージが表示されます。
アプリをデプロイする
この時点でのフォルダ構造は次のようになっているはずです。
.├── blog│ ├── app│ ├── bin... (application files and folders)├── deployer.rb├── Dockerfile次は、アプリを実行してデプロイしましょう。
$ ruby deployer.rb deployコマンドが実行されるたびに出力が表示されます。すべての出力結果は、最初の例の手動実行とほぼ同じです。
Sending build context to Docker daemon 4.846 MBStep 1/9 : FROM ruby:2.3.1-slim ---> e523958caea8Step 2/9 : COPY Gemfile* /tmp/ ---> Using cache ---> f103f7b71338Step 3/9 : WORKDIR /tmp ---> Using cache ---> f268a864efbcStep 4/9 : RUN gem install bundler && apt-get update && apt-get install -y build-essential libsqlite3-dev rsync nodejs && bundle install --path vendor/bundle ---> Using cache ---> 7e9c77e52f81Step 5/9 : RUN mkdir -p /app/vendor/bundle ---> Using cache ---> 1387419ca6baStep 6/9 : WORKDIR /app ---> Using cache ---> 9741744560e2Step 7/9 : RUN cp -R /tmp/vendor/bundle vendor ---> Using cache ---> 5467eeb53bd2Step 8/9 : COPY application.tar.gz /tmp ---> b2d26619a73cRemoving intermediate container 9835c63b601bStep 9/9 : CMD cd /tmp && tar -xzf application.tar.gz && rsync -a blog/ /app/ && cd /app && RAILS_ENV=production bundle exec rake db:migrate && RAILS_ENV=production bundle exec rails s -b 0.0.0.0 -p 3000 ---> Running in 8fafe2f238f1 ---> c0617746e751Removing intermediate container 8fafe2f238f1Successfully built c0617746e751The push refers to a repository [docker.io/mydockeruser/application-container]e529b1dc4234: Pushed08ee50f4f8a7: Layer already exists33e5788c35de: Layer already existsc3d75a5c9ca1: Layer already exists0f94183c9ed2: Layer already existsb58339e538fb: Layer already exists317a9fa46c5b: Layer already existsa9bb4f79499d: Layer already exists9c81988c760c: Layer already existsc5ad82f84119: Layer already existsfe4c16cbf7a4: Layer already existsaa2da7a: digest: sha256:a9a8f9ebefcaa6d0e0c2aae257500eae5d681d7ea1496a556a32fc1a819f5623 size: 2627aa2da7a: Pulling from mydockeruser/application-container1fad42e8a0d9: Already exists5eb735ae5425: Already existsb37dcb8e3fe1: Already exists50b76574ab33: Already existsc87fdbefd3da: Already existsf1fe764fd274: Already exists6c419839fcb6: Already exists4abc761a27e6: Already exists267a4512fe4a: Already exists18d5fb7b0056: Already exists219eee0abfef: Pulling fs layer219eee0abfef: Verifying Checksum219eee0abfef: Download complete219eee0abfef: Pull completeDigest: sha256:a9a8f9ebefcaa6d0e0c2aae257500eae5d681d7ea1496a556a32fc1a819f5623Status: Downloaded newer image for mydockeruser/application-container:aa2da7a01d777ef8d9ac3ecfc9a06701551f31641e4ece78156d4d90fcdaeb6141bf6367b3428a2c46f出力結果は、ハッシュやDockerキャッシュの違いによって異なることがあります。最後に、上のように2つのハッシュが出力されます。
01d777ef8d9ac3ecfc9a06701551f31641e4ece78156d4d90fcdaeb6141bf6367b3428a2c46f1つ目の短いハッシュは、停止したコンテナのハッシュです。最後の長いハッシュは、新たに実行中のコンテナのハッシュです。
これで、リモートサーバーのIPアドレスにアクセスするとアプリが実行されていることを確認できます。
Semaphoreで継続的デリバリー(CD)する
本チュートリアルのスクリプトを使って、アプリをSemaphoreに自動デプロイできます。やり方を見てみましょう。
最初に、「Project Settings」でDocker support付きのプラットフォームを指定します。
SemaphoreのProjectページで、「Set Up Deployment」をクリックします。
「Generic Deployment」を選択します。
「Automatic」を選択します。
Gitのブランチを選択します(普通はmaster)。
ここではアプリをデプロイしたいだけなので、ローカルコンピュータで実行するときと同じ方法でデプロイスクリプトを実行します。
rbenv global 2.3.1docker-cache restoreruby deployer.rb deploydocker-cache snapshot2つのdocker-cacheコマンドにご注目ください。これらがビルドしたイメージを取り出しを行うので、ゼロからビルドする必要はありません。ローカルでの実行と同様、最初は少し時間がかかりますが、次回からは速くなります。詳しくはSemaphoreの公式ドキュメントをご覧ください。
また、rbenv global 2.3.1コマンドをメモしておきましょう。これは、スクリプトの実行に必要な現在のRubyのバージョンを設定するためのものです。別の言語を使う場合は、必要な環境を設定する必要があります。
次の手順では、リモートサーバーへのアクセスに使うSSHキーのアップロード(必要な場合)と、新しいサーバーへの名前付けを行っています。完了すると、コードをmasterブランチにpushするたびにこのスクリプトが実行され、定義済みのリモートサーバーにアプリがデプロイされます。
その他の自動化可能なコマンド
この後のセクションでは、便利な自動化コマンドをいくつかご紹介します。
現在のバージョン
現在実行中のアプリのバージョンをトラックするには、コンテナのTagに情報を記述します。
現在実行中のバージョンを取り出すには、以下のコードが必要です。
def current remote_revision = `#{ssh_command} docker ps | grep -v CONTAINER | awk '{print $2}' | rev | cut -d: -f1 | rev`.strip abort('No running application.') if remote_revision == '' current_rev = `git show --ignore-missing --pretty=format:'%C(yellow)%h\%C(blue)<<%an>> %C(green)%ad%C(yellow)%d%Creset %s %Creset'\#{running_revision} | head -1`.strip if current_rev.empty? puts 'Local revision not found, please update your master branch.' else puts current_rev end deploy_by = `#{ssh_command} docker ps --format={{.Names}}` puts "Deploy by: #{deploy_by}"end各行の動作について解説します。
remote_revision = `#{ssh_command} docker ps | grep -v CONTAINER | awk '{print $2}' | rev | cut -d: -f1 | rev`.strip上のコマンドは以下を行います。
docker psでリモートコンテナのステータス出力を取得grep -v CONTAINERで出力からヘッダを除去awk '{print $2}'で2番目のカラム(image name:tag)を取得- 残りのコマンドでimage nameと
:を削除し、残りの部分とコミットハッシュを返す - 返された文字列の最終行の改行を
.stripで削除
abort('No running application.') if remote_revision == ''コンテナが1つも実行されていない場合や、コミットが1つも見つからない場合はコマンド実行をやめます。
current_rev = `git show --ignore-missing --pretty=format:'%C(yellow)%h\%C(blue)<<%an>> %C(green)%ad%C(yellow)%d%Creset %s %Creset'\#{running_revision} | head -1`.stripこのコマンドは、git logにマッチするコンテナハッシュを検索して書式を整えます。
if current_rev.empty? puts 'Local revision not found, please update your master branch.'else puts current_revendこのコミットが現在のgit historyにない場合、ユーザーにリポジトリの更新を促します。これは、新しいコミットがローカルコピーからまだrebaseされていない場合に発生することがあります。コミットがある場合は、ログ情報を出力します。
deploy_by = `#{ssh_command} docker ps --format={{.Names}}`このコマンドは、現在実行中のコンテナ名を返します。コンテナ名にはユーザー名とタイムスタンプが含まれます。
puts "Deploy by: #{deploy_by}"上のコマンドは、デプロイを行ったユーザーとタイムスタンプを出力します。
ログ
多くのアプリはログを出力するので、場合によってはログの面倒も見なければなりません。Dockerに組み込まれているログシステムを使うと、シンプルなSSH接続でアプリのログに簡単にアクセスできるようになります。
アプリからログを出力するには、以下を入力します。
def logs puts 'Connecting to remote host' system("#{ssh_command} 'docker logs -f --tail 100 \$(docker ps -q)'")enddocker logsコマンドは、アプリで生成されたログをすべて出力します。-fフラグは、接続を保持してすべてのログをストリームとして読み出せるようにします。--tailフラグは、出力する古いログの最大行数を指定します。最後の$(docker ps -q)は、リモートホストで実行中のコンテナごとにIDを返します。今はアプリを実行しているだけなので、コンテナをすべて取り出しても問題ありません。
メモ: 本記事のサンプルアプリはすべてのログをファイルに書き込むので、Dockerにはログを一切出力しません。この振る舞いは、アプリの起動時にRAILS_LOG_TO_STDOUT=true環境変数で変更できます。
Dockerのインストールとログイン
新しいホストでは、必要なインストールや設定をsetupコマンド一発でできるようにすると便利です。
インストールとログインの2つの手順を完了させます。
def docker_setup puts 'Installing Docker on remote host' system("#{ssh_command} -t 'wget -qO- https://get.docker.com/ | sh'") puts 'Adding the remote user to Docker group' system("#{ssh_command} 'sudo usermod -aG docker #{HOST_USER}'") puts 'Adding the remote user to Docker group' system("#{ssh_command} -t 'docker login}'")end各コマンドの動作について解説します。
system("#{ssh_command} -t 'wget -qO- https://get.docker.com/ | sh'")このコマンドはDockerのインストールスクリプトを実行します。リモートユーザーのパスワード入力を促すには-tフラグが必要です。パスワード入力を求められたら入力します。
system("#{ssh_command} 'sudo usermod -aG docker #{HOST_USER}'")このコマンドは、Dockerグループにリモートユーザーを追加します。これは、sudoせずにdockerコマンドを実行する場合に必要です。
system("#{ssh_command} -t 'docker login'")更新されたアプリをダウンロードするためにログインが必要なので、このコマンドが必要になります。-tフラグは、ログイン入力できるようにするためのものです。
ロールバック
新しいアプリの実行で何か問題が起きたら、直前のバージョンにいつでもロールバックできることが重要です。Dockerコンテナのアプローチを用いたことで、デプロイされたすべてのバージョンがホスト上に保存されているので、即座にロールバックを開始できます。
次のコードスニペットをご覧ください。
def rollback puts 'Fetching last revision from remote server.' previous_revision = `#{ssh_command} docker images | grep -v 'none\|latest\|REPOSITORY' | awk '{print $2}' | sed -n 2p`.strip abort('No previous revision found.') if previous_revision == '' puts "Previous revision found: #{previous_revision}" puts "Restarting application!" system("#{ssh_command} 'docker stop \$(docker ps -q)'") system("#{ssh_command} docker run --name #{deploy_user} #{APPLICATION_CONTAINER}:#{previous_revision}")end各手順の動作について見てみましょう。
puts 'Fetching last revision from remote server.' previous_revision = `#{ssh_command} docker images | grep -v 'none\|latest\|REPOSITORY' | awk '{print $2}' | sed -n 2p`.strip abort('No previous revision found.') if previous_revision == ''このコマンドは、リモートホスト上にあるすべてのDockerイメージの中から直前のコンテナtagをgrepします。このタグはgitコミットの短いハッシュになっていて、アプリのロールバックを参照するときに使われます。直前のDockerイメージがない場合は、ロールバックをやめます。
system("#{ssh_command} 'docker stop \$(docker ps -q)'")このコマンドは、実行中のコンテナをすべてシャットダウンして、直前のコンテナを起動できるようにします。
system("#{ssh_command} docker run --name #{deploy_user} #{APPLICATION_CONTAINER}:#{previous_revision}")このコマンドは、直前の手順で見つかったタグを用いてアプリを起動します。デプロイメソッド(deploy_user)で使われているのと同じ命名ルールを利用できます。
まとめ
本チュートリアルのすべての手順を行うと、ソフトウェアをデプロイする自動ツールが完全に動くようになるはずです。このツールは、アプリを簡単にデプロイできなければならないが、Herokuなどの自動化された環境にホスティングできない場合に便利です。
このツールが有用だとお思いいただけましたら、お気軽に本チュートリアルを共有してください。疑問点などがございましたら、ぜひ元記事にコメントをどうぞ。
皆さまが楽しくリリースできますように。
追伸: Dockerを用いた継続的デリバリー(CD)にご関心がおありでしたら、SemaphoreのDocker platformをぜひチェックしてください。タグ付きのDockerイメージのレイヤキャッシュを完全にサポートしています。
関連記事
hachi8833
X:@hachi8833GitHub:@hachi8833コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。これまでにRuby on Rails チュートリアル第2版のコンテンツ監修、Railsガイドのコンテンツ作成を担当。かと思うと、正規表現の粋を尽くした日本語エラーチェックサービスenno.jpを運営。Claude Codeに夢中になりすぎないための方法を模索中。ブログ:note.com/hachi8833、Amazonウィッシュリスト:https://bit.ly/32aAmiI
hachi8833の書いた記事一覧へ
本記事の内容へのお問い合せはTwitterで@techrachoへMentionまたはDMにてご連絡頂くか、運営会社であるBPS株式会社のお問い合せフォームよりお問い合せ下さい。




















