Docker上でPerlのWebアプリケーションを動かしてみる

この記事はモバイルファクトリー 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のインストールについてはこの記事では扱いません。
必要であれば[公式ガイド](https://docs.docker.com/engine/installation/)に従ってインストールしてください。
また、[fish](https://fishshell.com/)というシェルを使っていますが、bashやzshでも動作すると思います。

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

```shell-session
$ 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` or `docker run` )
Dockerでは、**Dockerコンテナ**という環境の中でアプリケーションが動きます。
ひとまず実行してみましょう。
定番のhello worldです。

```shell-session
$ 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` コマンドでは、イメージからコンテナを作成し、そのコンテナ上でコマンドを実行します。

```shell-session
$ docker container run --help

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

Run a command in a new container

(後略)
```

先のhello worldでは、[`perl:5.26` のDockerイメージ](https://hub.docker.com/_/perl/)をベースにしたコンテナ上で `perl` コマンドが実行されています。

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

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

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

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

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

```shell-session
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` コマンドで実行中のコンテナ一覧を見ることができます。

```shell-session
$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
```

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

```shell-session
$ 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` コマンドを使います。

```shell-session
$ 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` )でコンテナを指定します。

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

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

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

```shell-session
$ 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` コマンドで削除できます。

```shell-session
$ 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アプリケーションを構築する

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

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

`Carton` と `Amon2` をインストールします。

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

(中略)

Successfully installed Amon2-6.13
90 distributions installed
```

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

```shell-session
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` します。

```shell-session
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アプリケーションを動かすことができます。
では起動してみましょう。

```shell-session
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でアクセスしてみます。

```shell-session
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アプリケーションのプロセスを殺します。

```shell-session
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アプリケーションを起動してみましょう。

```shell-session
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は適宜読み替えてください）

```shell-session
root@a0e8801dd74d:/app/HelloWorld# exit

$ docker stop a0e8801dd74d

$ docker commit a0e8801dd74d amon2_helloworld
```

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

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

```shell-session
$ docker run -p 5000:5000 -it amon2_helloworld bash
```

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

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

```shell-session
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!](https://qiita-image-store.s3.amazonaws.com/0/151514/9b134b74-62c4-0aee-11e8-3b59ea38d760.png)

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

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

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

では `Dockerfile` を作りましょう。

ホスト側で適当にディレクトリを作り、 `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` というイメージを作成できます。

```shell-session
$ docker build -t amon2_helloworld .
```

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

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

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

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

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