Docker Composeで3層ウェブアーキテクチャを構成する

Tech

2019.3.15

Topics

 こんにちは。データサイエンスチームのtmtkです。
 この記事では、3層ウェブアプリケーションを構築することを通してDocker Composeの使い方をお伝えします。

はじめに

 Docker Composeは複数のDockerコンテナを一つのホストマシンに立ち上げることができるツールです。公式ドキュメントでは用途の例として開発環境の構築やCI、CDへの応用が挙げられています。
 3層ウェブアプリケーションは、ウェブサーバ、アプリケーションサーバ、データベースサーバの3層から構築されるウェブアプリケーションです。
 この記事では、3層ウェブアプリケーションをDocker Composeで構築する手順を追うことで、DockerやDocker Composeの使い方を体得することを目的にしています。


(今回構築する3層ウェブアプリケーションの全体像)

実習

 まず、Ubuntu 18.04が入っているサーバを用意します。AWSのEC2インスタンスをつかってもいいですし、手元のパソコンで仮想マシンを構築してもいいです。

Dockerのインストール

 Dockerの公式ドキュメントのとおりにDockerをインストールします。

sudo apt-get update
sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

 Post-installation steps for Linuxも実行しておきます。

sudo groupadd docker
sudo usermod -aG docker $USER
exit

 公式ドキュメントのとおり、Docker Composeをインストールします。

sudo curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

Docker Composeを使う

 ここで、Docker Composeの作業用のディレクトリを作っておきます。

mkdir dockercompose_techblog
cd dockercompose_techblog

データベースサーバを立てる

 データベースサーバとして、MySQLが動くコンテナを構築します。
 Docker Hubのmysqlのページを参照しながら、MySQLサーバを構築します。まずはページの説明どおりにMySQLのコンテナを動かしてみます。以下のコマンドで、MySQL 5.7のコンテナにdatabaseという名前をつけ、ルートユーザのパスワードを「my-secret-pw」にして立ち上げます。

docker run --name database -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:5.7

 コンテナ内で作業するために、コンテナ内でBashを立ち上げます。

docker exec -it database /bin/bash

 コンテナ内でMySQLに接続し、データベースの操作方法を確認します。

mysql --user=root --password=my-secret-pw
mysql> CREATE DATABASE test_database;
mysql> USE test_database;
mysql> CREATE TABLE test_table (name VARCHAR(255), english INT, mathematics INT);
mysql> INSERT INTO test_table (name, english, mathematics) VALUES ("Ichiro", 10, 10), ("Jiro", 20, 40), ("Saburo", 30, 90);
mysql> SELECT * FROM test_table;
+--------+---------+-------------+
| name   | english | mathematics |
+--------+---------+-------------+
| Ichiro |      10 |          10 |
| Jiro   |      20 |          40 |
| Saburo |      30 |          90 |
+--------+---------+-------------+
3 rows in set (0.00 sec)

test_databaseという名前でデータベースを作成し、そこにtest_tableというテーブルを作成し、上のようなデータを入れることができました。動作確認ができたので、コンテナ内のMySQLとBashから抜けます。

mysql > exit
exit

 立ち上げたDockerのコンテナを終了しておきます。

docker kill database

 先ほどのDocker Hubのページに以下のような説明があります。要約すると、/docker-entrypoint-initdb.d以下に置いた.sh, .sql, .sql.gzファイルはコンテナ起動時に実行されるということが書いてあります。

Initializing a fresh instance
When a container is started for the first time, a new database with the specified name will be created and initialized with the provided configuration variables. Furthermore, it will execute files with extensions .sh, .sql and .sql.gz that are found in /docker-entrypoint-initdb.d. Files will be executed in alphabetical order. You can easily populate your mysql services by mounting a SQL dump into that directory and provide custom images with contributed data. SQL files will be imported by default to the database specified by the MYSQL_DATABASE variable.

 この情報をつかって、先ほど試したMySQLのコンテナイメージをベースに、自分専用のコンテナイメージを作成します。具体的には、コンテナが立ち上がるとtest_databaseデータベース、test_tableテーブル、それに上と同様のデータが作成されるようにします。
 データベースサーバ用のディレクトリを作成します。

mkdir database
cd database

 先ほど試した操作を再現するシェルスクリプトを作成します。init.shとして次のような内容のファイルをつくります。

#! /bin/bash

COMMAND='
CREATE DATABASE test_database;
USE test_database;
CREATE TABLE test_table (name VARCHAR(255), english INT, mathematics INT);
INSERT test_table (name, english, mathematics) VALUES ("Ichiro", 10, 10), ("Jiro", 20, 40), ("Saburo", 30, 90);'

mysql --user=root \
      --password=my-secret-pw \
      --execute="$COMMAND"

 このシェルスクリプトがコンテナの起動時に実行されるように、Dockerfileを作成します。Dockerfileとして次の内容のファイルを作ります。

FROM mysql:5.7

COPY init.sh /docker-entrypoint-initdb.d/

 このDockerfileからコンテナイメージをビルドします。

docker build . -t database

 先ほど立ち上げたdatabaseという名前のMySQLサーバは、docker killで終了はしているものの、削除はされていないため、docker container rmコマンドで削除します。

docker container rm database

 先ほどビルドしたコンテナイメージからコンテナを起動します。

docker run --name database -e MYSQL_ROOT_PASSWORD=my-secret-pw -d database

 データベースコンテナが準備できました。親ディレクトリに戻っておきます。

cd ..

アプリケーションサーバの構築

 ここからはアプリケーションサーバを構築していきます。その前に、先ほど起動したデータベースサーバへの接続方法を確認しておきましょう。
 データベースサーバのIPアドレスを確認するため、docker networkコマンドを使います。ネットワークの一覧を確認します。

docker network ls

 databaseコンテナはこの中でbridgeネットワークにあります。IPアドレスを調べます。

docker network inspect bridge

 このコマンドの出力を調べると、私の環境ではdatabaseコンテナのIPアドレスは172.17.0.2となっていました。Pythonからこのデータベースに接続する方法を確認します。
 Python 3からデータベースに接続するために、必要なものをインストールします。

sudo apt install python3-pip python3-dev default-libmysqlclient-dev
sudo pip3 install mysqlclient==1.4.2.post1

 Python 3処理系を起動し、データベースに接続するテストを行います。

python3
import MySQLdb
db_settings = {"host": "172.17.0.2", "user": "root", "passwd": "my-secret-pw", "db": "test_database", "charset": "utf8mb4"}
db_conn = MySQLdb.connect(**db_settings)
cursor = db_conn.cursor()
query = "SELECT name, english, mathematics FROM test_table"
cursor.execute(query)
ret = cursor.fetchall()
ret
exit()
(('Ichiro', 10, 10), ('Jiro', 20, 40), ('Saburo', 30, 90))

データベースサーバへの接続とデータの取得が確認できました。
 アプリケーションサーバで実行するプログラムを作成します。アプリケーションサーバのコンテナ用の作業ディレクトリを作成し、移動します。

mkdir app_server
cd app_server

ここに、app_server.pyとして以下の内容のファイルを作成します。

""" Make HTML page with data from database """
import logging
from flask import Flask
import MySQLdb

app = Flask(__name__)
logging.basicConfig(format="%(asctime)s [%(levelname)s] %(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)

@app.route("/")
def root():
    """ Root page """

    db_setting = {
        "host": "database",
        "user": "root",
        "passwd": "my-secret-pw",
        "db": "test_database",
        "charset": "utf8mb4"
        }

    data = []
    try:
        db_conn = MySQLdb.connect(**db_setting)
        try:
            cursor = db_conn.cursor()
            query = "SELECT name, english, mathematics FROM test_table"
            cursor.execute(query)
            data = cursor.fetchall()
        finally:
            db_conn.close()
    except Exception as err:
        logger.error("%s %s", type(err), err)

    body = "<table><tr><th>name</th><th>english</th><th>mathmatics</th></tr>{}</table>".format(
        "".join([
            "<tr><td>{}</td><td>{}</td><td>{}</td>".format(name, english, mathematics)
            for name, english, mathematics
            in data
        ])
        )

    page = "<html><head><title>Test Page</title></head><body>{body}</body></html>\n"

    return page.format(body=body)

HTTPリクエストがあるとデータベースサーバに接続し、データを取得し、それをテーブルに載せたHTMLを返すFlaskによるウェブアプリケーションです。これを実行するコンテナを定義するDockerfileを作成します。Dockerfileとして以下の内容を保存します。

FROM ubuntu:18.04

RUN sed -i.bak -e "s%http://archive.ubuntu.com/ubuntu/%http://jp.archive.ubuntu.com/ubuntu/%g" /etc/apt/sources.list

RUN apt update && apt upgrade -y && apt install python3 python3-pip python3-dev libmysqlclient-dev -y

RUN pip3 install mysqlclient==1.4.2.post1 flask==0.12.1 gunicorn==19.9.0

COPY . /app

WORKDIR /app

CMD while true; do gunicorn --bind 0.0.0.0:80 app_server:app ; done

アプリケーションサーバのコンテナの準備ができました。親ディレクトリに戻ります。

cd ..

Docker Composeを試す

 ここまででデータベースサーバとアプリケーションサーバが準備できました。まだウェブサーバが準備できていませんが、ここまでできた内容でDocker Composeを試してみます。
 docker-compose.ymlとして、以下を保存します。

version: "2"
services:
  database:
    build:
      context: database
    networks:
      - default
    environment:
      - MYSQL_ROOT_PASSWORD=my-secret-pw
  app_server:
    build:
      context: app_server
    links:
      - database
    networks:
      - default

アプリケーションサーバからデータベースサーバにアクセスできるよう、

  app_server
    links:
      - database

とネットワークの設定をしています。
 Docker Composeを使って、データベースサーバとアプリケーションサーバの二つのコンテナをビルド・起動します。

docker-compose build
docker-compose up -d

 各コンテナが吐き出すログはdocker-compose logsで確認できます。

docker-compose logs

 アプリケーションサーバにアクセスしてみます。アプリケーションサーバのIPアドレスを知るために、再びdocker networkコマンドを使います。docker network lsコマンドを実行すると、dockercompose_techblog_defaultというネットワークがあることがわかると思います。
 次のコマンドの出力をみれば、アプリケーションサーバのIPアドレスがわかります。

docker network inspect dockercompose_techblog_default

 私の環境の場合は172.18.0.3でした。cURLコマンドかブラウザでアプリケーションサーバの動作を確認します。

curl http://172.18.0.3 # or firefox http://172.18.0.3
<html><head><title>Test Page</title></head><body><table><tr><th>name</th><th>english</th><th>mathmatics</th></tr><tr><td>Ichiro</td><td>10</td><td>10</td><tr><td>Jiro</td><td>20</td><td>40</td><tr><td>Saburo</td><td>30</td><td>90</td></table></body></html>

アプリケーションサーバが正常に動作していることがわかります。

ウェブサーバを構築する

 3層ウェブアプリケーションを実現するため、ウェブサーバを構築します。作業用ディレクトリをつくり、移動します。

mkdir web_server
cd web_server

 Docker Hubにあるnginxのイメージを使うことにします。nginxの設定ファイルは/etc/nginx/conf.d/default.confにあるので、ホスト側に設定ファイルをコピーしておきます。

docker run --name nginx -d nginx:1.15
docker cp nginx:/etc/nginx/conf.d/default.conf default.conf.bak

 コピーした設定ファイルを確認すると、以下のような内容になっていることがわかります。

less default.conf.bak
server {
    listen       80;
    server_name  localhost;

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

 nginxをリバースプロキシとして動作させる設定ファイルを作ります。このファイルをコピーします。

cp default.conf.bak default.conf

 default.conf

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

の部分を

    location / {
        proxy_pass http://app_server;
    }

と編集します。こうすることで、アプリケーションサーバへのアクセスをウェブサーバが代理することができます。
 最後に、Dockerfileを作成します。以下の内容でDockerfileを作成します。

FROM nginx:1.15

COPY default.conf /etc/nginx/conf.d/default.conf

 これでウェブサーバの準備ができました。親ディレクトリに戻ります。

cd ..

3層ウェブアプリケーションをDocker Composeで構築する

 さきほど作成したdocker-compose.ymlを編集し、ウェブサーバを追加することで、3層ウェブアプリケーションを実現します。docker-compose.ymlweb_serverの項目を追加し、以下のようにします。

version: "2"
services:
  database:
    build:
      context: database
    networks:
      - default
    environment:
      - MYSQL_ROOT_PASSWORD=my-secret-pw
  app_server:
    build:
      context: app_server
    links:
      - database
    networks:
      - default
  web_server:
    build:
      context: web_server
    links:
      - app_server
    ports:
      - "80:80"
    networks:
      - default

 ホストへのポート80番のアクセスをweb_serverコンテナに転送する設定にしています。
 コンテナたちをビルド・起動します。

docker-compose build
docker-compose up -d

 cURLやブラウザからウェブサーバにアクセスし、動作確認をします。

curl http://localhost # or firefox http://localhost


無事に動作が確認できました。
 docker-compose logsでログを確認すると、ウェブサーバへのアクセスがnginxのアクセスログとして取得できていることが確認できます。

web_server_1  | 172.18.0.1 - - [20/Feb/2019:06:31:29 +0000] "GET / HTTP/1.1" 200 257 "-" "curl/7.58.0" "-"

 コンテナ一つ一つのログをみるには、docker psコマンドでコンテナの名前を確認し、docker logsコマンドを使います。以下はweb_serverのログを見る例です。

docker ps
docker logs dockercompose_techblog_web_server_1

作業終了

 作業を終了するため、Docker Composeから立ち上げたコンテナを終了します。

docker-compose down

 念のため、起動したまま残っているコンテナがないか確認します。

docker ps

不要なコンテナが残っていた場合は、docker killで終了させることができます。

docker kill *some_container_name*

 Dockerを使っているとコンテナイメージがディスクを圧迫することがあります。docker container prunedocker image pruneを使うと、終了したが削除されていないコンテナや、使われてないDockerイメージを削除できるので、必要に応じて使うといいでしょう。

まとめ

 この記事では、Docker Composeで3層ウェブアプリケーションを作成する手順を説明しました。Dockerのコマンドはたくさんあってややこしいですが、この記事で実際的な使い方とともに説明したことが少しでも役に立てば幸いです。

tmtk

データ分析と機械学習とソフトウェア開発をしています。 アルゴリズムとデータ構造が好きです。

Recommends

こちらもおすすめ

Special Topics

注目記事はこちら