この記事はモバイルファクトリー Advent Calendar 2017の1日目の記事です。
@carimaticsが担当します。

本記事はDocker入門として、Docker上でPerlを動かすというお話です。
雰囲気くらいは掴めると思います。

Perlの入門ではないです。( carton などは知ってるものとします)

開発環境

以下の環境で動作確認をしています。

  • OS: macOS Sierra version 10.12.6
  • Docker: Version 17.09.0-ce-mac35
  • fish: version 2.7.0

Dockerのインストールについてはこの記事では扱いません。
必要であれば公式ガイドに従ってインストールしてください。
また、fishというシェルを使っていますが、bashやzshでも動作すると思います。

はじめに

本記事の内容

前述の通り、本記事はDocker上でPerlを動かすという内容です。
とは言うものの、それだけであればコマンド一発でおしまいです。(後で少し詳しく解説します)

$ docker run perl:5.26 perl -e 'print "Hello, Perl on Docker!\n"'
Hello, Perl on Docker!

これだけでは面白味に欠けるので、Webアプリケーションを動かしてみようと思います。

なぜDocker上でPerlを動かすのか

Dockerを使う利点はいくつかありますが、今回の目的はホスト環境をPerlで汚さないためです。
CPANモジュールを検証したい時などに使えます。
また、いつでもPerlの環境を捨てることができるので非常に良いです。

Docker入門

Dockerコンテナを作成、起動する( docker container run of docker run )

Dockerでは、Dockerコンテナという環境の中でアプリケーションが動きます。
ひとまず実行してみましょう。
定番のhello worldです。

$ docker container run perl:5.26 perl -e 'print "Hello, Perl on Docker!\n"'
Hello, Perl on Docker!

初回の実行では perl:5.26 というイメージをダウンロードする必要があるため、少し時間がかかります。
2回目以降の実行はほぼノータイムで実行できるはずです。

上記のコマンドで出力されたのは perl -e 'print "Hello, Perl on Docker!\n"' の実行結果です。
perlの実行環境があれば、これだけで同様の出力結果を得ることができます。

docker run コマンドでは、イメージからコンテナを作成し、そのコンテナ上でコマンドを実行します。

$ docker container run --help

Usage:  docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

Run a command in a new container

(後略)

先のhello worldでは、perl:5.26 のDockerイメージをベースにしたコンテナ上で perl コマンドが実行されています。

perl:5.26 は、Dockerの公式イメージで、perl v5.26の環境などが含まれています。

コンテナ内のシェルを実行してみます。

$ docker container run -it perl:5.26 bash
root@a0e8801dd74d:~# 

bash コマンドを実行しました。
-it オプションにより、pseudo-TTYからコンテナの標準入力にアクセスできます。
コマンドを実行した次の行には、pseudo-TTYのプロンプト( root@a0e8801dd74d:~# のような感じの)が表示され、普段のシェルに対して行っているのと同様に、コンテナで起動している bash に対して標準入力が行えます。

試しに幾つかコマンドを叩いてみましょう。

root@a0e8801dd74d:~# perl --version

This is perl 5, version 26, subversion 0 (v5.26.0) built for x86_64-linux

root@a0e8801dd74d:~# cpanm --version
cpanm (App::cpanminus) version 1.7043 (/usr/local/bin/cpanm)
perl version 5.026000 (/usr/local/bin/perl)

root@a0e8801dd74d:~# carton
bash: carton: command not found

root@a0e8801dd74d:~# ls /app
ls: cannot access '/app': No such file or directory

root@a0e8801dd74d:~# exit
exit

コマンドの実行結果は適宜省略しています。
コンテナには carton コマンドや /app ディレクトリは存在しないようです。

コンテナ一覧を見る( docker container ls or docker ps )

さて、 exit コマンドでコンテナから抜けましたが、現在コンテナはどうなっているでしょう。
docker container ls コマンドで実行中のコンテナ一覧を見ることができます。

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

実行中のコンテナは存在しないようです。
-a オプションですべての状態のコンテナを見ることができます。

$ docker container ls -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                           PORTS               NAMES
a0e8801dd74d        perl:5.26           "bash"              2 minutes ago       Exited (0) 2 minutes ago                             agitated_wiles

コンテナの状態( STATUS )は Exited になっています。

停止したコンテナを再起動する( docker container start or docker start )

Exited のコンテナを再び起動するには docker container start コマンドを使います。

$ docker container start a0e8801dd74d
a0e8801dd74d

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
a0e8801dd74d        perl:5.26           "bash"              12 minutes ago      Up 5 seconds                            agitated_wiles

docker container start ではコンテナID( CONTAINER ID )かコンテナ名 ( NAMES )でコンテナを指定します。
コンテナIDやコンテナ名は適宜自身の環境のものと読み替えてください。

docker exec コマンドで、実行中のコンテナに対してコマンドを実行できます。
コンテナID( CONTAINER ID )かコンテナ名 ( NAMES )でコンテナを指定します。

$ docker exec -it agitated_wiles bash
root@a0e8801dd74d:~# exit
exit

これまで触れませんでしたが、プロンプトに含まれる謎の文字列はコンテナIDです。

docker container run すると、これまでのものとは別のコンテナが作成されます。

$ docker container run -it perl:5.26 bash
root@d10e3b0e5f55:~# exit
exit

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
d10e3b0e5f55        perl:5.26           "bash"              8 seconds ago       Exited (0) 4 seconds ago                       modest_mcnulty
a0e8801dd74d        perl:5.26           "bash"              25 minutes ago      Up 12 minutes                                  agitated_wiles

コンテナを削除する( docker container rm or docker rm )

停止したコンテナは、 docker container rm コマンドで削除できます。

$ docker container rm d10e3b0e5f55
d10e3b0e5f55

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
a0e8801dd74d        perl:5.26           "bash"              28 minutes ago      Up 15 minutes                           agitated_wiles

PerlのWebアプリケーションを構築する

コンテナ内に適当なディレクトリを作成して移動します。

$ docker exec -it a0e8801dd74d bash
root@a0e8801dd74d:~# mkdir /app
root@a0e8801dd74d:~# cd /app

CartonAmon2 をインストールします。

root@a0e8801dd74d:/app# cpanm Carton Amon2
--> Working on Carton

(中略)

Successfully installed Amon2-6.13
90 distributions installed

続いてAmon2アプリケーションのセットアップをします。

root@a0e8801dd74d:/app# amon2-setup.pl HelloWorld
-- Running flavor: Basic --

(中略)

Initialized empty Git repository in /app/HelloWorld/.git/

*** Please tell me who you are.

Run

  git config --global user.email "you@example.com"
  git config --global user.name "Your Name"

to set your account's default identity.
Omit --global to set the identity only in this repository.

fatal: unable to auto-detect email address (got 'root@07ab9031e56d.(none)')
32768 at /usr/local/lib/perl5/site_perl/5.26.0/Amon2/Setup/VC/Git.pm line 21.

git の設定をしていないので怒られました。
まぁ今回は無視して無事セットアップができたということにしましょう。

HelloWorld ディレクトリが作成されているので、移動して carton install します。

root@a0e8801dd74d:/app# cd HelloWorld
root@a0e8801dd74d:/app/HelloWorld# carton install
Installing modules using /app/HelloWorld/cpanfile

(中略)

125 distributions installed
Complete! Modules were installed into /app/HelloWorld/local

これで取り敢えずWebアプリケーションを動かすことができます。
では起動してみましょう。

root@a0e8801dd74d:/app/HelloWorld# carton exec -- perl script/helloworld-server
HelloWorld: http://127.0.0.1:5000/

起動しました!
http://127.0.0.1:5000/ で待ち受けているようですね。
ループバックアドレスなので外部からは接続できません。

Ctrl-c で抜けた後、無理矢理バックグラウンドプロセスとして動かしてcurlでアクセスしてみます。

root@a0e8801dd74d:/app/HelloWorld# carton exec -- perl script/helloworld-server &
[1] 31559

root@07ab9031e56d:/app/HelloWorld# curl http://127.0.0.1:5000/
<!doctype html>
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>HelloWorld</title>

(後略)

どうやらHelloWorldが動いている気配がします。
しかしやっぱりブラウザで確認したいですね。

Webアプリケーションのプロセスを殺します。

root@a0e8801dd74d:/app/HelloWorld# pkill perl
[1]+  Exit 4                  carton exec -- perl script/helloworld-server

手始めに cpanfile を書き換えて carton install します。

root@a0e8801dd74d:/app/HelloWorld# sed -i "2i requires 'IO::Interface::Simple';" cpanfile

root@a0e8801dd74d:/app/HelloWorld# carton install

続いて script/helloworld-server を書き換えます。

root@a0e8801dd74d:/app/HelloWorld# sed -i '8i use IO::Interface::Simple;' script/helloworld-server

root@a0e8801dd74d:/app/HelloWorld# sed -i -E "s;('127\.0\.0\.1');IO::Interface::Simple->new('eth0')->address // \1;" script/helloworld-server

再びWebアプリケーションを起動してみましょう。

root@a0e8801dd74d:/app/HelloWorld# carton exec -- perl script/helloworld-server
HelloWorld: http://172.17.0.2:5000/

今度はループバックアドレスではなく、 http://172.17.0.2:5000/ で待ち受けています。
ホストのブラウザからWebアプリケーションを確認しましょう。

コンテナから抜け、以下のコマンドを叩きます。(コンテナIDは適宜読み替えてください)

root@a0e8801dd74d:/app/HelloWorld# exit

$ docker stop a0e8801dd74d

$ docker commit a0e8801dd74d amon2_helloworld

docker commit で既存のコンテナから新しいイメージを作成します。
ここでは、ID a0e8801dd74d のコンテナから amon2_helloworld という名前のイメージを作成しました。

次に、作成したイメージを起動します。

$ docker run -p 5000:5000 -it amon2_helloworld bash

これまでと違い、今回は -p <host port>:<container port> オプションがついています。
こうすることで、ホストのポートにアクセスしたとき、コンテナのポートにフォワーディングされます。

では、再びアプリケーションを実行します。

root@3acd4e32f229:~# cd /app/HelloWorld/

root@3acd4e32f229:/app/HelloWorld# carton exec -- perl script/helloworld-server
HelloWorld: http://172.17.0.2:5000/

ホストのブラウザから http://localhost:5000 にアクセスします。

Hello, Amon2 World!

実行できました!すばらしい!

Dockerfileを使ってWebアプリケーションを構築する

Dockerfile を作成することで、上記でWebアプリケーション構築の際に実行した操作を繰り返し行なったり、手順を変更したりすることが容易になります。

では Dockerfile を作りましょう。

ホスト側で適当にディレクトリを作り、 Dockerfileという名前のファイルに以下のように記述します。

Dockerfile
FROM perl:5.26

WORKDIR /app

RUN cpanm Carton Amon2 \
    && amon2-setup.pl HelloWorld || :

WORKDIR /app/HelloWorld

RUN sed -i "2i requires 'IO::Interface::Simple';" cpanfile \
    && carton install \
    && sed -i '8i use IO::Interface::Simple;' script/helloworld-server \
    && sed -i -E "s;('127\.0\.0\.1');IO::Interface::Simple->new('eth0')->address // \1;" script/helloworld-server

CMD ["carton", "exec", "--", "perl", "script/helloworld-server"]

これまでの作業で内容は理解できると思うので、説明は最低限だけにします。

&& amon2-setup.pl HelloWorld || :

この行では、 git の設定がされていないために出るエラーを握り潰しています。
良い子はマネしないでくださいね。

CMD ["carton", "exec", "--", "perl", "script/helloworld-server"]

最後の行で、 docker run したときに実行されるデフォルトのコマンドを指定しています。

この Dockerfile があるディレクトリで、以下のコマンドを叩くと amon2_helloworld というイメージを作成できます。

$ docker build -t amon2_helloworld .

docker run を実行すると、構築作業で最後に起動したものと同じWebアプリケーションが起動します。

$ docker run -p 5000:5000 amon2_helloworld
HelloWorld: http://172.17.0.2:5000/

まとめ

本記事では、Dockerの基本的な操作を学びました。
また、Docker上でWebアプリケーションを構築し、実行できることを確認しました。
さらに、Dockerfileを利用することで、構築手順を残しておくことができます。

最初に想定していたよりもボリュームのある記事になってしまいました。
ここまで読んでいただきありがとうございます。
この記事で少しでもDockerに興味を持つ方が増えたならすごく嬉しいです。

モバイルファクトリー Advent Calendar 2017は、2日目も引き続き @carimatics が担当します。
よろしくお願いします。