1台のサーバ上にDockerを使って複数サービス運用してみる

Dockerを使ってProductionを運用してみて数ヶ月。
以下の様な構成で(今のところ)安定して動いているので、備忘録としてまとめておきます。
DBのコンテナ化も考えましたが、データ永続化の面などでちょっと不安だったのでやめました。

## なぜDockerを選択したか 
まず背景として、大人の事情で1台のサーバ上に複数サービスを運用する必要がありました。
ApacheやTomcatを、別インスタンスで稼働させる手も考えましたが、
将来の移行コストやポート番号の管理問題もあり、Dockerを選択しました。
(というか、これならDockerでいけそう。やってみたい！というモチベーションがあったのが大きいです)

## 前提
* serviceA、serviceBをサーバ1台で運用
* serviceA、serviceBで同じデータベースを参照
* ドメイン部分は固定(ここではhoge.jpとする)。サブドメインを分けて、複数サービスを運用
    * serviceA -> `https://serviceA.hoge.jp/....`
    * serviceB -> `https://serviceB.hoge.jp/....`
* SSL通信
* serviceAをバージョンアップする際、serviceBは停止しないようにする(逆も同じ)

## 構成
![Network diagram.png](https://qiita-image-store.s3.amazonaws.com/0/85701/fda45786-0a5a-aff1-5386-4fb8a3400863.png)

### nginx

* 役割
    * リバースプロキシ
    * serviceAに対するリクエストはlocalhost:49998へ、serviceBに対するリクエストはlocalhost:49999へ振り分けます
        * 49998や49999は空いているポート番号の例です。適当に変えて下さい。
    * SSLを解き、以降(サーバ内)の通信はhttpとします
    * httpアクセスはhttpsにリダイレクトさせます
* 設定ファイルの内容↓

        ### serviceA ###
        server {
            listen 80;
            server_name serviceA.hoge.jp;
            location / {
                rewrite ^(.*)$ https://$host$1 permanent;
            }
        }
        server {
            listen 443;
            server_name serviceA.hoge.jp;
            ssl on;
            ssl_certificate {serviceAのSSLサーバ証明書パス};
            ssl_certificate_key {秘密鍵パス};

            location / {
                # 49998は後述のdockerコンテナのポート番号
                proxy_pass http://localhost:49998;
                proxy_redirect http:// https://;
                # 接続元情報を維持
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-Proto https;
                proxy_set_header X-Forwarded-Host $host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            }
        }

        ### serviceB ###
        server {
            listen 80;
            server_name serviceB.hoge.jp;
            location / {
                rewrite ^(.*)$ https://$host$1 permanent;
            }
        }
        server {
            listen 443;
            server_name serviceB.hoge.jp;
            ssl on;
            ssl_certificate {serviceBのSSLサーバ証明書パス};
            ssl_certificate_key {秘密鍵パス};

            location / {
                # 49999は後述のdockerコンテナのポート番号
                proxy_pass http://localhost:49999;
                proxy_redirect http:// https://;
                # 接続元情報を維持
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-Proto https;
                proxy_set_header X-Forwarded-Host $host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            }
        }


### docker-compose
* サービス毎にコンテナの管理や起動・停止など行いたかったので、"docker-compose"を使用しています
* serviceAのdocker-compose.yml

        # tomcat
        tomcat:
          build: tomcat
          container_name: serviceA-tomcat
          stdin_open: true
          tty: true
          volumes:
            - /etc/localtime:/etc/localtime:ro
            - /var/log/docker-container/serviceA/tomcat:/var/log/tomcat8:rw
          extra_hosts:
            - "docker-host:${DOCKER_HOSTIP}"

        # apache
        apache:
          build: apache
          container_name: serviceA-apache
          stdin_open: true
          tty: true
          volumes:
            - /etc/localtime:/etc/localtime:ro
            - /var/log/docker-container/serviceA/apache:/var/log/apache2:rw
          links:
            - tomcat
          ports:
            - "49998:80"

* ポイント
    * extra_hostsの設定では、"docker-host"という名前でコンテナからホストを参照できるようにしています
        * ${DOCKER_HOSTIP}は、以下シェルのワンライナーで取得してホストの環境変数に設定してあります

                $ ip route show | grep docker0 | awk \"{print \\\$9}\"

    * 49998でポートを公開しています。nginxのserviceA設定で指定するポート番号になります
    * volumesでは、タイムゾーンとログ出力ディレクトリをマウントしています

* serviceBのdocker-compose.ymlも上記と同様に作成します。違いはservice名(A->B)とports部分(49998->49999)になります

### dockerコンテナ(Ubuntu15.10)
* serviceA-Apache
    * DockerFile

            FROM ubuntu:15.10

            MAINTAINER xxx <xxx@xxxx.xx>

            RUN rm -rf /var/lib/apt/lists/* && \
                apt-get clean && \
                apt-get update && \
                apt-get install -y apache2 \
                                   vim \
                                   less \
                                   net-tools \
                                   nmap \
                                   curl \
                                   nkf \
                                   inetutils-ping

            COPY serviceA-apache.conf /etc/apache2/sites-available/

            RUN a2ensite serviceA-apache.conf && \
                a2enmod proxy proxy_http rewrite

            ## set locale
            RUN locale-gen ja_JP.UTF-8
            ENV LANG ja_JP.UTF-8
            ENV LANGUAGE ja_JP:en
            ENV LC_ALL ja_JP.UTF-8

            EXPOSE 80

            ENV APACHE_RUN_USER    www-data
            ENV APACHE_RUN_GROUP   www-data
            ENV APACHE_PID_FILE    /var/run/apache2.pid
            ENV APACHE_RUN_DIR     /var/run/apache2
            ENV APACHE_LOCK_DIR    /var/lock/apache2
            ENV APACHE_LOG_DIR     /var/log/apache2

            ## tail -f ... はコンテナが途中終了しないためのおまじない命令
            ENTRYPOINT service apache2 start && tail -f /dev/null

    * serviceA-apache.conf(docker-composeでlinkさせているtomcatコンテナへリクエストを投げます)

                .
                .
            <Location />
                ProxyPass http://serviceA-tomcat:8080/
            </Location>
                .
                .

* serviceA-Tomcat
    * DockerFile

            FROM ubuntu:15.10

            MAINTAINER xxx <xxx@xxxx.xx>

            RUN rm -rf /var/lib/apt/lists/* && \
                apt-get clean && \
                apt-get update && \
                apt-get -y upgrade && \
                apt-get install -y vim \
                                   less \
                                   net-tools \
                                   nmap \
                                   curl \
                                   nkf \
                                   inetutils-ping

            ## java8,tomcat8
            ENV JAVA_HOME /usr/lib/jvm/java-8-oracle
            RUN echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections && \
                apt-get install -y software-properties-common && \
                add-apt-repository -y ppa:webupd8team/java && \
                apt-get update && \
                apt-get install -y oracle-java8-installer \
                                   tomcat8
            ENV PATH $PATH:$JAVA_HOME/bin

            ## set locale
            RUN locale-gen ja_JP.UTF-8
            ENV LANG ja_JP.UTF-8
            ENV LANGUAGE ja_JP:en
            ENV LC_ALL ja_JP.UTF-8

            ## tomcat
            ENV CATALINA_HOME /usr/share/tomcat8
            ENV CATALINA_BASE /var/lib/tomcat8

            RUN mkdir -p $CATALINA_BASE/bin && \
                chown -R tomcat8:tomcat8 $CATALINA_BASE/bin

            RUN mkdir -p $CATALINA_HOME/common/classes && \
                mkdir -p $CATALINA_HOME/server/classes && \
                mkdir -p $CATALINA_HOME/shared/classes && \
                mkdir -p $CATALINA_HOME/temp && \
                rm -rf $CATALINA_BASE/common && \
                rm -rf $CATALINA_BASE/server && \
                rm -rf $CATALINA_BASE/shared && \
                rm -rf $CATALINA_BASE/temp && \
                rm -rf $CATALINA_BASE/webapps/ROOT && \
                ln -s $CATALINA_HOME/common $CATALINA_BASE/common && \
                ln -s $CATALINA_HOME/server $CATALINA_BASE/server && \
                ln -s $CATALINA_HOME/shared $CATALINA_BASE/shared && \
                ln -s $CATALINA_HOME/temp $CATALINA_BASE/temp

            EXPOSE 8080

            ## tail -f ... はコンテナが途中終了しないためのおまじない命令
            ENTRYPOINT $CATALINA_HOME/bin/startup.sh && tail -f /dev/null


* serviceB-Apache
    * 上記と同様に作成
* serviceB-Tomcat
    * 上記と同様に作成

### postgreSQL
* コンテナからpostgreSQLへの接続方法は以下になります(JDBC接続、ポート5432の場合)

        jdbc:postgresql://docker-host:5432/

* "docker-host"はdocker-composeのextra_hostsで設定しています

## サービス起動

    $ cd {docker-compose.ymlがあるディレクトリ}
    $ docker-compose up -d

## DevOps
* 本サービスでは以下ツールを使用しています。この辺りの詳細は後日また書こうと思ってます。
    * Itamae(プロビジョニング)
    * Jenkins(CI)
    * Fablic(デプロイ)
    * idobata(チャット)
    * Flyway(DBマイグレーション)

## まとめ
パフォーマンスや障害発生時の切り分けなど、不安だった点はいくつかありましたが、
現状それに関しても問題なく運用出来ています。
運用・開発者がDockerに関する知識を最低限持つ必要がありますが、結果Dockerで構築してみて良かったと思ってます。
サービスによっては、インフラ構築の際の有力な選択肢としてDockerを加えてもいいと感じています。

## 追記(2017/01/25)
* 本番運用していて少し困ったこと
    * yumやaptのパッケージアップデート時にDockerに更新がかかった場合、Dockerコンテナが停止してしまいます
    * パッケージアップデートでDockerを除外してしまうと、カーネルが更新された時とか不具合起こしそうだからやめた方がよさそう(Dockerはホストとコンテナでカーネルが共有されている)