Railsアプリ経由でDockerを動かす

Dockerを使って作ってみたいWebサービスがあるので、そのために勉強し直してみようと思う。

まずは、Vagrant上に遊び場を用意する。

mkdir docker_playground; cd docker_playground
vagrant init centos/7
vagrant up
vagrant ssh

VMにsshで入ったら、CentOS7用のDockerをインストールする。

参考:https://docs.docker.com/engine/installation/linux/centos/

$ sudo yum update
$ curl -fsSL https://get.docker.com/ | sh
$ sudo usermod -aG docker vagrant
$ sudo service docker start
$ exit 
$ vagrant ssh 
$ sudo docker run hello-world #=> Make sure it is working

ここまででDockerの準備は終了。

次に、Vagrant内に必要なRailsプロジェクトを作り、Dockerとやり取りする。まずは「Dockerイメージの作成」「コンテナでコマンドを叩いてアウトプットを表示」の2つだけをできるようにしてみたい。

ということで、Railsのインストールが必要になる。

# On Vagrant VM 
$ sudo yum install -y patch git gcc readline-devel openssl-devel nginx gcc-c++ bzip2 vim sqlite3-devel
$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
$ rbenv install 2.2.4; rbenv global 2.2.4
$ gem install rails --no-ri --no-doc

JavaScriptのランタイムも必要なので、NodeJSを入れておく。

$ curl -L git.io/nodebrew | perl - setup
$ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile
$ source ~/.bash_profile
$ nodebrew install-binary v5.11.0
$ nodebrew alias default v5.11.0; nodebrew use default

さて、いよいよRailsプロジェクトを作成する。

$ rails new DockerOnRails; cd DockerOnRails
$ rails s -b 0.0.0.0

ここまでで、Railsのデフォルトページがホストマシンからアクセスできればとりあえずオーケー。

ここからDockerを作成・管理できるようにしていく。Rubyからdockerイメージ・コンテナにアクセスできるdocker-apiというGemがあるのでそれを試したいが、まずは対応するDocker自体のコマンドを確認しておこう。まずはこれから行ってみよう。

$ docker run docker/whalesay cowsay boo 
 _____
< boo >
 -----
    \
     \
      \
                    ##        .
              ## ## ##       ==
           ## ## ## ##      ===
       /""""""""""""""""___/ ===
  ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ /  ===- ~~~
       \______ o          __/
        \    \        __/
          \____\______/
$ irb
irb) require 'docker-api'
irb) image = Docker::Image.create('fromImage' => 'docker/whalesay')
irb) image.run('cowsay boo')

動いてるっぽいんだが、アウトプットを受け取ることができない。コンテナで試してみよう。

irb) container = Docker::Container.create('Image' => 'docker/whalesay', 'Cmd' => ['cowsay', 'boo'])
irb) container.start
irb) container.streaming_logs(stdout: true) { |stream, chunk| puts chunk }
 _____
< boo >
 -----
    \
     \
      \
                    ##        .
              ## ## ##       ==
           ## ## ## ##      ===
       /""""""""""""""""___/ ===
  ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ /  ===- ~~~
       \______ o          __/
        \    \        __/
          \____\______/
=> " _____ \n< boo >\n ----- \n    \\\n     \\\n      \\     \n                    ##        .            \n              ## ## ##       ==            \n           ## ## ## ##      ===            \n       /\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"___/ ===        \n  ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ /  ===- ~~~   \n       \\______ o          __/            \n        \\    \\        __/             \n          \\____\\______/   \n"

見事に表示された。あとはこれをRailsアプリ内で行えば良いということみたい。

それをやるためのRailsアプリ側の設計を考えてみよう。全体をContainersControllerで管理する。
ContainersControllerは#createで新しいコンテナを作成し、#indexで一覧を返し、#showで個々のコンテナを表示することができる。そして、#runアクションでコマンドを走らせ、アウトプットを受け取ることができる。こんな感じだろうか。

やってみよう。

$ rails g scaffolding Container image:string container_id:string; rake db:migrate

あとはコントローラのメソッドにContainerを作り出す命令を加えればオーケー。

  def show
    @docker_container = Docker::Container.get(@container.container_id)
    @docker_container.start
  end

  def create
    @container = Container.new(container_params)
    image = 'docker/whalesay'
    docker_container = Docker::Container.create('Image' => image, 'Cmd' => ['cowsay', 'boo'])
    @container.image = image
    @container.container_id = docker_container.id

    respond_to do |format|
      if @container.save
        format.html { redirect_to @container, notice: 'Container was successfully created.' }
        format.json { render :show, status: :created, location: @container }
      else
        format.html { render :new }
        format.json { render json: @container.errors, status: :unprocessable_entity }
      end
    end
  end

そして、View(app/views/containers/show.html.erb)に簡単なレンダリング。

 <p>
 <strong>Logs:</strong>
 <%= simple_format(@docker_container.streaming_logs(stdout: true)) %>
</p>

だいぶ崩れてるけど、動いているみたい。

Screen Shot 2016-04-24 at 16.46.48

当初の目標であった「コマンドと叩いてそのアウトプットを得る」こともできたのだが、それほどシンプルにうまくいかなかったので、それに関しては後日改めてまとめてみようと思う。