Movatterモバイル変換


[0]ホーム

URL:


Tech Rachoエンジニアの「?」を「!」に。
  1. TOP
  2. 開発
  3. Rails: DockerでHeroku的なデプロイソリューションを構築する: 後編(翻訳)
  • 開発

Rails: DockerでHeroku的なデプロイソリューションを構築する: 後編(翻訳)

前記事:Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

概要

原著者の許諾を得て、CC BY-NC-SAライセンスに基づき翻訳・公開いたします。

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.43
if 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つのハッシュが出力されます。

01d777ef8d9ac3ecfc9a06701551f31641e4ece78156d4d90fcdaeb6141bf6367b3428a2c46f

1つ目の短いハッシュは、停止したコンテナのハッシュです。最後の長いハッシュは、新たに実行中のコンテナのハッシュです。

これで、リモートサーバーの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 snapshot

2つの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)'")end

docker 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イメージのレイヤキャッシュを完全にサポートしています。

関連記事

Rails: DockerでHeroku的なデプロイソリューションを構築する: 前編(翻訳)

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)


この記事を書いた人

hachi8833

X:@hachi8833GitHub:@hachi8833コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。これまでにRuby on Rails チュートリアル第2版のコンテンツ監修、Railsガイドのコンテンツ作成を担当。かと思うと、正規表現の粋を尽くした日本語エラーチェックサービスenno.jpを運営。Claude Codeに夢中になりすぎないための方法を模索中。ブログ:note.com/hachi8833Amazonウィッシュリスト:https://bit.ly/32aAmiI

hachi8833の書いた記事一覧へ

本記事の内容へのお問い合せはTwitterで@techrachoへMentionまたはDMにてご連絡頂くか、運営会社であるBPS株式会社のお問い合せフォームよりお問い合せ下さい。

Our Services

各種サービスのご依頼やお問い合わせなど、お気軽にご相談ください。

Our Products

製品のご利用希望や疑問・質問など、お気軽にご相談ください。

Recruit & Contacts

お問い合わせ、採用へのお申し込みはこちらから。

積極採用中 開発エンジニアCONTACT

関連記事

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。
CONTACT FORM

[8]ページ先頭

©2009-2025 Movatter.jp