【ChromeOSインストール版】Dockerを公式チュートリアル”Get Started with Docker”で学ぶ【前編】

ChromeOSにDockerをインストールしてみたので公式のドキュメントを見ながら学んで見たいと思います。

【参考】 uepon.hatenadiary.com

正直QiitaなどではDockerのMAC版の資料は検索に引っかかってくるのですが、LinuxというかChromeOSでの情報が見つかりにくい状況だったので、 いいタイミングなので公式のチュートリアルをみながら学んで見ようと思います。

【公式チュートリアル

docs.docker.com

f:id:ueponx:20190227225438p:plain

Get Started, Part 1: Orientation and setup

このパートではDockerの説明とセットアップに関して記載されています。

チュートリアルの流れは以下のようになっているようです。

  1. Docker環境を設定する
  2. 1つのコンテナとしてイメージを構築する
  3. 複数のコンテナを実行するようにアプリをスケールする
  4. クラスタ全体にアプリを分散する
  5. バックエンドデータベースを追加してスタックサービス化
  6. アプリをプロダクションに

個人的には3つ目ぐらいで理解の限界がきそうなのですが、できるだけ頑張ってみます。

この章の最初の方は以下の説明がされています。

  • Dockerとはどんなものなのか?
  • イメージとコンテナとは?
  • コンテナとVMの違い

そのあたりは結構わかりやすいサイトもあるのでググっていただければ…とにかく利点はいっぱいあるのでとりあえず進めていきましょう。

インストール

DockerにはCEとEEの2つのエディションがあります。個人で使うとなるとCE(Community Edition )になると思います。EEはEnterprise Editionの略です。 (注)インストールするときにKubernetesを使う前提であればバージョンに注意が必要のようです。

インストールに関しては過去のエントリを参考にしてください。WindowsとかOSXな方はインストーラがあるので大丈夫でしょう。

【参考:Dockerのインストール】

uepon.hatenadiary.com

参考のエントリ内で、インストール後にバージョンの確認やhello-worldイメージの実行などは行っていますので、このパートはこれで終わりとなります。

Get Started, Part 2: Containers

次のパートででは実際にコンテナを作成して行くお話になります。 前提としてはDockerのインストールと動作に関しては事前確認しておくことですが、前パートが動作できていればOKです。

Dockerで考えるアプリはコンテナ、サービス、スタックというレイヤーがあり、その一番下層にくるのがコンテナになるそうです。

  • Stack
  • Services
  • Container (←ここをやる)

これまでの開発では環境づくりが結構めんどくせー感じだったので、それを解決するんだ〜って感じの内容になっています。 Dockerを使えば、開発環境はイメージの取得だけで、インストールは不要。開発側のアプリも、依存関係、そしてランタイムを含めて移行できるみたいです。

これらのイメージの設定はDockerfileと呼ばれるものによって定義します。 Dockerファイルで定義されていればどの環境に行っても同じ実行環境を再構築できるというのがメリットです。環境構築するのが目的ではないのでこれは便利!

今回のチュートリアルではpythonのアプリケーションを通してコンテナの扱い方を学びます。 まずは作業を行うディレクトリを作成して、移動します。

$ mkdir friendlyhello
$ cd friendlyhello

移動したディレクトリでdockerfileを作成していきます。チュートリアルのdockerfileのテキストをそのまま使用すればいいのですが、今回はこれを少し変えて進めます。 チュートリアルではdockerfileに記述するベースイメージがpython:2.7-slimとなっていますが、ChromeOSではリソースが小さいため Alpine Linuxpythonイメージを使用することになりました。これでかなりのディスクスペースの軽量化も図れます。

alpinelinux.org

【dockerfile】

# Use an official Python runtime as a parent image
# FROM python:2.7-slim # オリジナルの記述
FROM python:2.7-alpine

# Set the working directory to /app
WORKDIR /app

# Copy the current directory contents into the container at /app
COPY . /app

# Install any needed packages specified in requirements.txt
RUN pip install --trusted-host pypi.python.org -r requirements.txt

# Make port 80 available to the world outside this container
EXPOSE 80

# Define environment variable
ENV NAME World

# Run app.py when the container launches
CMD ["python", "app.py"]

これで定義できました。あとは、開発するアプリの本体app.pyと依存関係の部分(pipのインストール設定)requirements.txtを設定します。 基本的にはチュートリアルそのままで大丈夫です。

【requirements.txt】

Flask
Redis

今回のアプリではRedisというデータベースを使用しています。

Redisは、ネットワーク接続された永続化可能なインメモリデータベース。連想配列、リスト、セットなどのデータ構造を扱える。いわゆるNoSQLデータベースの一つ。オープンソースソフトウェアプロジェクトであり、Redis Labsがスポンサーとなって開発されている。(Wikipediaより

redis.io

自分も初めて使用するものなのであんまりよくわかっていないですが、自分が前々から欲しかった機能はこれかも。今後はRedisを使っていこうかな。

【app.py】

from flask import Flask
from redis import Redis, RedisError
import os
import socket

# Connect to 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)

ここまでできて、ディレクトリの内容が以下のような状態になったことを確認します

$ ls
Dockerfile      app.py          requirements.txt

続いてはdockerfileの情報をもとにビルドを行います。

$ docker build --tag=friendlyhello .

※ 末尾のドット.も忘れずに!

--tagオプションまたは-tオプションで作成するイメージにタグ付けができるので、今回はfriendlyhelloという名前をつけています。

【ログ】

$ docker build --tag=friendlyhello .
Sending build context to Docker daemon   5.12kB
Step 1/7 : FROM python:2.7-alpine
 ---> 028b1c040d1e
Step 2/7 : WORKDIR /app
 ---> Using cache
 ---> eae50d1926e5
Step 3/7 : COPY . /app
 ---> 5769976c4b92
Step 4/7 : RUN pip install --trusted-host pypi.python.org -r requirements.txt
 ---> Running in 19bcbd6b1e65
DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 won't be maintained after that date. A future version of pip will drop support for Python 2.7.
Collecting itsdangerous>=0.24 (from Flask->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/76/ae/44b03b253d6fade317f32c24d100b3b35c2239807046a4c953c7b89fa49e/itsdangerous-1.1.0-py2.py3-none-any.whl
Collecting Jinja2>=2.10 (from Flask->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/7f/ff/ae64bacdfc95f27a016a7bed8e8686763ba4d277a78ca76f32659220a731/Jinja2-2.10-py2.py3-none-any.whl (126kB)
Collecting MarkupSafe>=0.23 (from Jinja2>=2.10->Flask->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/b9/2e/64db92e53b86efccfaea71321f597fa2e1b2bd3853d8ce658568f7a13094/MarkupSafe-1.1.1.tar.gz
Building wheels for collected packages: MarkupSafe
  Building wheel for MarkupSafe (setup.py): started
  Building wheel for MarkupSafe (setup.py): finished with status 'done'
  Stored in directory: /root/.cache/pip/wheels/f2/aa/04/0edf07a1b8a5f5f1aed7580fffb69ce8972edc16a505916a77
Successfully built MarkupSafe
Installing collected packages: Werkzeug, click, itsdangerous, MarkupSafe, Jinja2, Flask, Redis
Successfully installed Flask-1.0.2 Jinja2-2.10 MarkupSafe-1.1.1 Redis-3.2.0 Werkzeug-0.14.1 click-7.0 itsdangerous-1.1.0
Removing intermediate container 5d012755c159
 ---> 9fbdfb77b37c
Step 5/7 : EXPOSE 80
 ---> Running in cc219cd8b3ab
Removing intermediate container cc219cd8b3ab
 ---> 4b2f986a13a9
Step 6/7 : ENV NAME World
 ---> Running in 69f49359148c
Removing intermediate container 69f49359148c
 ---> a602cc6d6905
Step 7/7 : CMD ["python", "app.py"]
 ---> Running in 005ad4362edb
Removing intermediate container 005ad4362edb
 ---> 1ad3fbdd80bf
Successfully built 1ad3fbdd80bf
Successfully tagged friendlyhello:latest

Dockerファイルでは以下のようなことを行っています。

  1. イメージの取得(ローカルにない場合にはDocker Hubから取得)
  2. 作業用ディレクトリの設定
  3. ローカルで開発したアプリのファイルをイメージにコピー
  4. pipでアプリが依存するモジュール環境構築
  5. 公開ポートの設定
  6. 環境変数の設定
  7. アプリの実行

イメージ作成結果の確認すると…

$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
friendlyhello       latest              1ad3fbdd80bf        40 minutes ago      73MB
python              2.7-alpine          028b1c040d1e        4 days ago          61.2MB

イメージが2つになっていますが、Alpain Linuxのベースイメージを取得してから、それに追加する形でオリジナルのイメージを作成するので、 ローカル側には実質2つのイメージが保存されます。

タグがlatestになっています。 tagオプションの完全な構文は--tag = friendlyhello:v0.0.1と記述するようです。 ベースにAlpine Linuxのイメージを利用していることもあり、イメージのサイズも非常に小さくなっています。

ここまででimageの作成が完了できたので、以降は実行となります。

注意点

Proxyサーバ経由の環境の場合

Proxyサーバの設定が必要な場合にはdockerfileに以下の行を追加してProxyのポートを設定する必要があります。

# Set proxy server, replace host:port with values for your servers
ENV http_proxy host:port
ENV https_proxy host:port

DNSの設定ミスがあった場合

DNSの設定ミスでpipの実行がエラーになった場合には /etc/docker/daemon.jsonに以下のようにDNSの設定を追加してdockerのプロセスを再起動し、再度buildを行ってください。

/etc/docker/daemon.json

{
  "dns": ["your_dns_address", "8.8.8.8"]
}

【dockerサービスの再起動】

$ sudo service docker restart

アプリケーションの実行

いよいよアプリケーションを実行します。実行にはdocker runコマンドを使用します。 その際、-pオプションを使用して、ローカルマシンのポート4000をアプリケーションのコンテナマシンのポート80にマッピングします。 この指定を行うことでローカルマシンのhttp://localhost:4000/のアクセスを行うとコンテナマシンのhttp://localhost:80/にアクセスしたことと同じように動作させることができます。多分Linuxでこの動作を行う際はポート番号に注意が必要のようです。port4000ではエラーが発生したので、port8080などの比較的大きな番号のものに差し替えたほうがいいかと思います。

$ docker run -p 8080:80 friendlyhello

【ログ】

$ docker run -p 8080:80 friendlyhello
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:80/ (Press CTRL+C to quit)

動作後にローカルPC側のブラウザからhttp://localhost:8080/へアクセスします。

f:id:ueponx:20190227225542p:plain

【ログ】

$ docker run -p 8080:80 friendlyhello
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:80/ (Press CTRL+C to quit)
172.17.0.1 - - [26/Feb/2019 15:12:43] "GET / HTTP/1.1" 200 -
172.17.0.1 - - [26/Feb/2019 15:12:45] "GET /favicon.ico HTTP/1.1" 404 -

動作が確認できたらコンソールで【Ctrl+C】として終了します。

アプリはデタッチモードでも起動することもできます。(イメージ起動後がバックグラウンドで行われています。)

$ docker run -d -p 8080:80 friendlyhello

実行を確認するためにはdocker container lsを実行して状況を確認します。

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                  NAMES
56b0547a29b9        friendlyhello       "python app.py"     37 seconds ago      Up 33 seconds       0.0.0.0:8080->80/tcp   determined_joliot

このように表示されます。この状態で先ほどと同様にブラウザからのアクセスにも応答され動作を確認することもできます。

デタッチモードでの動作を終了する場合には、docker container lsコマンド実行時に表示されたCONTAINER IDを使用してdocker container stopコマンドで停止を行います。

【ログ】

$ docker run -d -p 8080:80 friendlyhello
56b0547a29b91f3ec0c14f02ce5beea34b67d7db24e415419270e25547c59d97

$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                  NAMES
56b0547a29b9        friendlyhello       "python app.py"     37 seconds ago      Up 33 seconds       0.0.0.0:8080->80/tcp   determined_joliot

$ docker container stop 56b0547a29b9
56b0547a29b9

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

f:id:ueponx:20190227225819p:plain

あれ?

先程、Dockerのコンテナの実行を行いましたが、なにか変な感じしませんでしたか?

f:id:ueponx:20190227225542p:plain

counter disabledってなんだよw なんと!公式のドキュメント内でも同じ表示になっています。

f:id:ueponx:20190228003531p:plain

この部分はapp.pyから使用されているRedisが動作できていないことに起因しているようです。 簡単に言うとRedisはインストールしているのにredis-serverを起動していないので、Redisの処理に失敗しているというわけです。

こりゃいかんということでdockerfileの見直しとアプリを起動するシェルスクリプトを作成します。 一見dockerfile内の末尾にあるCMDのエントリにredis-serverの起動を追加すればいいのではないかと思うのですが、CMDは設定に含まれる最後の1行のみが実行されるので 起動シェルを作成するのがよさそうです。

【startup.sh ファイル作成】

#!/bin/sh
redis-server &
python app.py

作成したファイルに実行権限付与をします。

chmod +x ./startup.sh

【dockerfile 要修正】

# Use an official Python runtime as a parent image
# FROM python:2.7-slim # オリジナルの記述
FROM python:2.7-alpine

# Set the working directory to /app
WORKDIR /app

# Copy the current directory contents into the container at /app
COPY . /app

# Install any needed packages specified in requirements.txt
RUN pip install --trusted-host pypi.python.org -r requirements.txt \
    && apk --update --no-cache add redis

# Make port 80 available to the world outside this container
EXPOSE 80

# Define environment variable
ENV NAME World

# Run app.py when the container launches
# CMD ["python", "app.py"] # オリジナルの記述
CMD ["./startup.sh"]

アプリ側の記述も修正を行います。Redisのホストとポート名の引数を変更・追加しています。

【app.py 要修正】

from flask import Flask
from redis import Redis, RedisError
import os
import socket

# Connect to Redis
#redis = Redis(host="redis", db=0, socket_connect_timeout=2, socket_timeout=2)
redis = Redis(host="localhost", port=6379, 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)

再ビルド、再実行を行う

f:id:ueponx:20190301082302p:plain

ブラウザでアクセスすると…

f:id:ueponx:20190301082319p:plain

無事動作できました。

作成したイメージの共有

こうして作成したイメージは別環境でも使用できるようにレジストリに登録して共有を行えるようにすれば利便性が上がります。 Gitに似たようなものだと思います。Dockerにもオリジナルのレジストリがありますが、それ以外のものも使用することは可能です。

まずは以下のサイトでDocker.Hubのユーザー登録を行います。

hub.docker.com

ユーザー登録が終わったら、コマンドラインで以下を実行し、Dockerの公開レジストリにログインを行います。

f:id:ueponx:20190303113325p:plain

無事にログインができたらレジストリへのレポジトリの関連付けを行うタグ登録を行います。 推奨のタグ付けの規則としてはusername/repository:tagという形式を使うようです。

【一般化】

$ docker tag image username/repository:tag

【例:ユーザー名がhogeの場合】

$ docker tag friendlyhello hoge/get-started:part2

実行がうまく行くと新しいタグイメージが生成されます。(確認はdocker image lsで行えます)

$ docker tag friendlyhello hoge/get-started:part2
$ docker image ls
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
hoge/get-started   part2               6321d6a7a1cd        2 days ago          74.4MB
friendlyhello        latest              6321d6a7a1cd        2 days ago          74.4MB
python               2.7-alpine          028b1c040d1e        8 days ago          61.2MB

f:id:ueponx:20190303113503p:plain

つづいてはイメージの公開になります。

【一般化】

$ docker push username/repository:tag

【例:ユーザー名がhogeの場合】

$ docker push hoge/get-started:part2

f:id:ueponx:20190303113423p:plain

この処理が完了するとpushしたイメージは一般に公開されます。以下のように実行すればローカルにイメージがなければ ネットからイメージを取得して実行が行われます。

$ docker run -p 8080:80 hoge/get-started:part2

実際にイメージを削除して実行すると以下のようになります。

f:id:ueponx:20190303113130p:plain

f:id:ueponx:20190303113020p:plain

とりあえず、これでパート2は終了です。

おわりに

長くなったので一旦ここで終了です。実際に触ってみた感じですが、ぼっち開発な自分でもかなり使えそうな感じです。 問題があるとすればネット回線が太いところでないとむずかしいのかなと。

【関連】 uepon.hatenadiary.com

/* -----codeの行番号----- */