Part 2:コンテナ

必要条件

皆さんの環境でセットアップが完了しているかどうか、素早く確認するには:

docker run hello-world

はじめに

Docker を使い、アプリケーションを作り始めましょう。コンテナを用いたアプリケーション階層の底部を、このページから始めます。このレベルの上位にあるのがサービスであり、プロダクションにおけるコンテナの挙動を定義します。こちらは Part3 で扱います。最終的にはスタックの頂上、つまり、 Part5 で扱うすべてのサービスの挙動を定義します。

  • スタック Stack
  • サービス Services
  • コンテナ(今ここにいます)

新しい開発環境

Python アプリケーションを書き始めるにあたり、自分のマシン上に Python ランタイムをインストールするのが、これまでは一番初めの仕事でした。しかし、サーバ上でもアプリケーションが期待する通りに問題なく動作するには、マシンと同じ環境を作成しなくてはいけません。

Docker であれば、移動可能な Python ランタイムをイメージ内に収容しているため、インストールは不要です。そして、ベース Python イメージにはアプリのコードも一緒に構築できますし、アプリを確実に動かすための依存関係やランタイムも全て運べます。

移動可能なイメージは Dockerifle と呼ばれるモノで定義します。

Dockerfile でコンテナの定義

Dockerfile では、コンテナ内の環境で何をするかを定義します。ネットワーク・インターフェースとディスク・ドライバののようなリソースは、システム上の他の環境からは隔離された環境内に仮想化されています。このようなリソースに接続するには、ポートを外の世界にマッピング(割り当て)する必要がありますし、どのファイルを環境に「複製」(copy in)するか指定する必要もあります。しかしながら、これらの作業を Dockerfile における構築時の定義で済ませておけば、どこで実行しても同じ挙動となります。

Dockerfile

空ディレクトリを作成します。新しいディレクトリ内にディレクトリを変更( cd )し、 Dockerfile という名前のファイルを作成し、以降の内容をファイルにコピー&ペーストし、保存します。なお、Dockerfile のコメントは、各命令文に対する説明です。

# 公式 Python ランタイムを親イメージとして使用
FROM python:2.7-slim

# 作業ディレクトリを /app に設定
WORKDIR /app

# 現在のディレクトリの内容を、コンテナ内の /app にコピー
ADD . /app

# requirements.txt で指定された必要なパッケージを全てインストール
RUN pip install -r requirements.txt

# ポート 80 番をコンテナの外の世界でも利用可能に
EXPOSE 80

# 環境変数の定義
ENV NAME World

# コンテナ起動時に app.py を実行
CMD ["python", "app.py"]

この Dockerfile は、 app.pyrequirements.txt といった、まだ作成していないファイルを参照しています。次はこれらを作りましょう。

アプリ自身

さらに2つのファイルを作成します。 requirements.txtapp.py です。これらを Dockerfile と同じフォルダに入れます。アプリは見ての通り、極めて単純になります。先ほどの Dockerfile でイメージの構築時、 DockerfileADD 命令で app.pyrequirements.txt をイメージの中に組み込みます。

  • requirements.txt
Flask
Redis
  • app.py
from flask import Flask
from redis import Redis, RedisError
import os
import socket

# Redis に接続
redis = Redis(host="redis", db=0, socket_connect_timeout=2, socket_timeout=2)

app = Flask(__name__)

@app.route("/")
def hello():
    try:
        visits = redis.incr("counter")
    except RedisError:
        visits = "<i>cannot connect to Redis, counter disabled</i>"

    html = "<h3>Hello {name}!</h3>" \
           "<b>Hostname:</b> {hostname}<br/>" \
           "<b>Visits:</b> {visits}"
    return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname(), visits=visits)

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=80)

先ほどの pip install -r requirements.txt で Python 用の Flask と Redis ライブラリをインストールします。そして、アプリは環境変数 NAME を表示し、また socket.gethostname() を呼び出した結果も出力します。しかしながら、 Redis は実行できないため(Python ライブラリをインストールしただけであり、 Redis 自身は入っていません)、実行を試みても失敗し、エラーメッセージを表示するでしょう。

注釈

コンテナ内でホスト名の取得を試みると、コンテナ ID を返します。コンテナ ID は実行バイナリにおけるプロセス ID のようなものです。

以上です! システム上に Python や requirements.txt に書かれているどれもが不要であり、それどころか、システム上にイメージの構築や実行も不要なのです。一見しますと環境に Python と Flask をインストールしていませんが、既に持っているのです。

アプリの構築

アプリを構築する準備が整いました。まだ、新しく作成したディレクトリのトップレベルにいるのを確認します。ここでは ls は次のようになるでしょう。

$ ls
Dockerfile           app.py                  requirements.txt

次は構築コマンドを実行します。これは Docker イメージを作成します。イメージには分かりやすい名前として -t でタグを指定します。

docker build -t friendlyhello .

構築したイメージはどこにあるのでしょうか? マシン上のローカルにある Docker イメージ・レジストリの中です。

$ docker images

REPOSITORY            TAG                 IMAGE ID
friendlyhello         latest              326387cea398

アプリの実行

アプリの実行にあたり、マシン側のポート 4000 をコンテナの公開ポート 80 に割り当てるには -p を使います。

docker run -p 4000:80 friendlyhello

Python がアプリに提供するのは http://0.0.0.0:80 であるのに注意して下さい。しかし、これはコンテナ内で表示されるメッセージであり、コンテナ内からはコンテナのポート 80 番からポート 4000 への割り当ては分かりません。適切な URL は http://localhost:4000 です。

ウェブブラウザで URL を開くと、「Hello World」文字列とコンテナ ID 、Redis エラーメッセージといった内容がウェブページに表示されます。

シェル上で curl コマンドを実行しても、同じ内容を表示します。

$ curl http://localhost:4000

<h3>Hello World!</h3><b>Hostname:</b> 8fc990912a14<br/><b>Visits:</b> <i>cannot connect to Redis, counter disabled</i>

注釈

このポート 4000:80 の再割り当ては、 DockerfileEXPOSE での指定とは異なるポートを指定できるデモです。ここでは、 docker run -p で何を公開( publish )するかを指定しました。後の手順では、ホストのポート 80 をコンテナ内のポート 80 に割り当て、 http://localhost で接続します。

ターミナル上で CTRL+C を実行し、終了します。

次はアプリをバックグラウンドで動作するため、デタッチド・モード(detached mode)で実行しましょう。

docker run -d -p 4000:80 friendlyhello

コマンドを実行しますと、アプリの長いコンテナ ID を表示し、ターミナルに戻ります。コンテナはバックグラウンドで実行中です。なお、 docker container ls で短縮コンテナ ID を確認できます(コマンド実行時は、長いコンテナ ID と短縮 ID のどちらも利用できます)。

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED
1fa4ab2cf395        friendlyhello       "python app.py"     28 seconds ago

このように http://localhost:4000 で表示したものと同じコンテナ ID ( CONTAINER ID )が表示されます。

あとは、プロセスを停止するために docker stop コマンドでコンテナ ID を次のように指定します。

docker stop 1fa4ab2cf395

イメージの共有

作成したイメージの移動性(ポータビリティ)を実証するため、イメージをアップロードし、どこかで動かしましょう。そのためには、コンテナをプロダクションにデプロイする時、どのようにレジストリに送信(push)するかを学ぶ必要があります。

レジストリ(registry)はリポジトリの集まりであり、リポジトリとはイメージの集まりです。これは GitHub リポジトリのようなものですが、コードが既に構築済みである点が異なります。レジストリのアカウント(利用者)は多くのリポジトリを作成できます。 docker コマンドライン・インターフェースは、デフォルトで Docker の公開リポジトリを使います。

注釈

ここでは無料に使えて設定済みの Docker 公開レジストリを使いますが、他の公開レジストリからもお選びいただけます。あるいは、 Docker Trusted Regsitry をセットアップしたら、自分のプライベートなレジストリも使えます。

Docker ID でログイン

Docker アカウントをお持ちでなければ、 cloud.docker.com でサインアップ(登録)します。そのとき、ユーザ名をお控えください。

自分のローカルマシンから Docker 公開レジストリにログインします。

docker login

イメージのタグ

ローカルのイメージとレジストリ上にあるリポジトリとを関連付ける概念は、 ユーザ名/リポジトリ:タグ です。タグはオプションですが、指定が推奨されています。これは、レジストリにおける Docker イメージのバージョン指定の仕組みに使うためです。指定するのは get-started:part のように、レポジトリ名と意味のあるタグ名です。こちらはイメージを get-started リポジトリに、タグを part1 として送信します。

次はイメージにタグをつけます。 docker tag image でユーザ名、リポジトリ、タグ名をしていすると、任意の場所へイメージをアップロードします。コマンドの構文は次の通りです。

docker tag image ユーザ名/リポジトリ:タグ

例:

docker tag friendlyhello john/get-started:part1

docker images で直近にタグ付けしたイメージを表示します。( docker image ls でも同様です)

$ docker images
REPOSITORY               TAG                 IMAGE ID            CREATED             SIZE
friendlyhello            latest              d9e555c53008        3 minutes ago       195MB
john/get-started         part1               d9e555c53008        3 minutes ago       195MB
python                   2.7-slim            1c7128a655f6        5 days ago          183MB
...

イメージの送信

タグ付けしたイメージをリポジトリにアップロードします。

docker push username/repository:tag

完了したら、アップロード結果が表示され、誰でも利用可能になります。 Docker Hub にログインしたら、pull コマンドで取得可能な新しいイメージが表示されます。

リモート・リポジトリにあるイメージの取得と実行

あとは docker run コマンドをつかい、あらゆるマシン上でアプリを実行できます。

docker run -p 4000:80 username/repository:tag

もしもイメージがマシン上のローカルに存在しなければ、 Docker はリポジトリから取得します。

docker image rm <iイメージ ID>
$ docker run -p 4000:80 john/get-started:part1
Unable to find image 'john/get-started:part1' locally
part1: Pulling from orangesnap/get-started
10a267c67f42: Already exists
f68a39a6a5e4: Already exists
9beaffc0cf19: Already exists
3c1fe835fb6b: Already exists
4c9f1fa8fcb8: Already exists
ee7d8f576a14: Already exists
fbccdcced46e: Already exists
Digest: sha256:0601c866aab2adcc6498200efd0f754037e909e5fd42069adeff72d1e2439068
Status: Downloaded newer image for john/get-started:part1
 * Running on http://0.0.0.0:80/ (Press CTRL+C to quit)

注釈

各コマンドで :タグ を指定しなければ、 :latest タグが指定されたものとみなされます。これは build 時も run 時も同様です。Docker はイメージに対するタグの指定がなければ(直近のイメージであれば不要です)、最新版を使います。

どこで docker run を実行したとしても、 Python と requirements.txt で指定した全ての依存関係と実行するコードが入ったイメージをダウンロード(pull)します。整った小さなパッケージで全てを持ち運びできます。そして、ホストマシン上では Docker さえ実行できれば、何もインストールする必要はありません。

パート2のまとめ

以上でこのページは終わりです。次のセクションでは、 サービス としてこのコンテナを実行し、アプリケーションをどのようにスケールするかを学びましょう。

まとめとチート・シート(オプション)

このページで扱ったターミナルの録画 がこちらです。

こちらはこのページで扱った Docker の基本コマンドと関連コマンドです。次に進む前に、試してみてはいかがでしょうか。

docker build -t friendlyname .               # このディレクトリ内にある DockerCile でイメージ作成
docker run -p 4000:80 friendlyname  # "friendlyname" の実行にあたり、ポート 4000 を 80 に割り当て
docker run -d -p 4000:80 friendlyname                            # 同じですが、デタッチド・モード
docker container ls                                                  # 全ての実行中コンテナを表示
docker container ls -a                                       # 停止中も含めて全てのコンテナを表示
docker container stop <hash>                                       # 指定したコンテナを丁寧に停止
docker container kill <hash>                               # 指定したコンテナを強制シャットダウン
docker container rm <hash>                                   # マシン上から指定したコンテナを削除
docker container rm $(docker container ls -a -q)                           # 全てのコンテナを削除
docker image ls -a                                               # マシン上の全てのイメージを表示
docker image rm <image id>                                       # マシン上の特定のイメージを削除
docker image rm $(docker image ls -a -q)                         # マシン上の全てのイメージを削除
docker login                                       # CLI セッションで Docker の認証を行いログイン
docker tag <image> username/repository:tag      # レジストリにアップロードする <image> にタグ付け
docker push username/repository:tag                                  # タグ付けしたイメージを送信
docker run username/repository:tag                               # レジストリにあるイメージを実行

参考

Get Started, Part 2: Containers | Docker Documentation
https://docs.docker.com/get-started/part2/