単純なDocker同士で疎結合なWebアプリ構築

記事タイトルとURLをコピーする

こんばんは、SWX3人目の熊谷(悠)です。
docker composeやソケットファイル無しでソケット通信を使用した nginxコンテナ ⇔ uWSGIコンテナ(Flaskアプリ)環境を構築する方法です。 ※本稿の設定例そのままの値だとIP直指定しており、疎結合とは到底言えない状態のため、最終的にはサービス毎にAPIエンドポイントを作成するなど設定値を置き換えて利用ください。

環境

Amazon ECS(別タスク定義)やDockerが起動しているマシン(RHEL7系Linux)にて動作を確認しました。
本稿では、例として公式AMIから起動したばかりで初期状態のAmazonLinux2上に構築します。

AmazonLinux2に構築するコマンド例

$ sudo su
# yum update -y
# yum -y install docker
# systemctl start docker
# systemctl enable docker
# systemctl status docker
# mkdir /opt/example-service

# mkdir /opt/example-service/app
# cd /opt/example-service/app/
# nano hello.py
# nano uwsgi.ini
# nano Dockerfile
# docker build -t hello -f Dockerfile .
# docker run -itd -p 3031:3031 hello

# mkdir /opt/example-service/web
# cd /opt/example-service/web/
# nano env_list
# nano uwsgi.conf.template
# nano Dockerfile
# docker build -t hello/nginx -f Dockerfile .
# docker run -itd --env-file=env_list -p 80:80 hello/nginx
# curl http://172.17.0.3/hello

App

Flaskアプリのファイルを作成します。 ※色々書いてますが関数はhelloだけでも大丈夫です。

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/healthcheck')
def healthcheck():
    return ""

@app.route("/hello")
def hello():
    return jsonify({
        "message": "Hello World!"
    })

@app.route('/api/v1/status/200', methods=['GET'])
def status_200():
    app.logger.info('execute 200 status')
    return jsonify({'message': '200 status message'}), 200

@app.route('/api/v1/status/400', methods=['GET'])
def status_400():
    app.logger.info('execute 400 status')
    return jsonify({'message': '400 status message'}), 400

@app.route('/api/v1/status/500', methods=['GET'])
def status_500():
    app.logger.info('execute 500 status')
    return jsonify({'message': '500 status message'}), 500
    
# Flaskのみで動作するビルトインサーバーを起動する※ローカルで動かす時用
if __name__ == '__main__':
    PORT = 5000
    app.run(
        threaded=True,
        debug=True,
        port=PORT,
        host='0.0.0.0',
    )

uWSGIの設定ファイルを作成します。

[uwsgi]
env=TZ=UTC-9
socket=0.0.0.0:3031 # nginxとのソケット通信用
#http=0.0.0.0:9090 # 単体(Webサーバー無し)で動作するHTTPサーバーを起動する※uWSGIコンテナに直接HTTPリクエストしたい時用
wsgi-file=./hello.py
master=true
callable=app

アプリを動かすためのDockerfileを作成します。

1行目をFROM python:latestにすれば3~9行目のパッケージインストールコマンド群は不要になりますが、コンテナイメージサイズは倍(421MB ⇒ 952MB)になります。

FROM python:alpine

RUN apk --update-cache add \
    gcc \
    g++ \
    build-base \
    linux-headers \
    python3-dev \
    pcre-dev

RUN pip install --upgrade pip \
    && pip install --no-cache-dir \
    Flask \
    uwsgi

COPY hello.py ./
COPY uwsgi.ini ./

EXPOSE 3031
CMD uwsgi uwsgi.ini

コンテナビルド&起動コマンド例

$ sudo docker build -t hello -f Dockerfile .

$ sudo docker run -itd -p 3031:3031 hello

Web

設定変更するためだけに毎回ビルドしなくて済むようにenvsubst用設定ファイルを作成します。(ECSなら環境変数に指定する。)

  • SERVER_NAME:nginxコンテナにアクセスするIPもしくはFQDN
  • RESOLVER:HOST_NAMEを解決できるレコードを持ったDNSサーバーIP
  • HOST_NAME:uWSGIコンテナのIPもしくはFQDN
  • PORT:uWSGIコンテナ側で待ち受けているwsgiソケットポート
SERVER_NAME=127.0.0.1
RESOLVER=169.254.169.253
HOST_NAME=172.17.0.2
PORT=3031

nginxの設定ファイルを作成します。

server {
    listen 80 default_server;
    server_name ${SERVER_NAME};

    location / {
        include uwsgi_params;
        resolver ${RESOLVER};
        set $url ${HOST_NAME};
        uwsgi_pass $url:${PORT};
    }

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

Dockerfileを作成します。

FROM nginx:alpine

RUN apk --no-cache add tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    apk del tzdata

COPY ["uwsgi.conf.template","/tmp/"]

EXPOSE 80
ENTRYPOINT ["/bin/sh","-c"]
CMD ["envsubst '$$SERVER_NAME $$RESOLVER $$HOST_NAME $$PORT' < /tmp/uwsgi.conf.template > /etc/nginx/conf.d/uwsgi.conf && nginx -g 'daemon off;'"]

コンテナビルド&起動コマンド例

$ sudo docker build -t hello/nginx -f Dockerfile .

$ sudo docker run -itd --env-file=env_list -p 80:80 hello/nginx

確認

nginxが起動しているコンテナにリクエストします。

$ curl http://172.17.0.3/healthcheck
$ curl http://172.17.0.3/hello
{"message":"Hello World!"}
$ curl http://172.17.0.3/api/v1/status/200
{"message":"200 status message"}
$ curl http://172.17.0.3/api/v1/status/400
{"message":"400 status message"}
$ curl http://172.17.0.3/api/v1/status/500
{"message":"500 status message"}

(ECS以外)uWSGI, nginxそれぞれのログを確認したい場合

$ sudo tail -f /var/lib/docker/containers/[コンテナID]/[コンテナID]-json.log

おまけ:docker-composeを用いる場合

docker networkのデフォルトサブネットアドレス帯と重複するのを避けるために、異なるアドレス帯を指定しています。 本稿例通りにenv_listのHOST_NAMEを設定されている場合は、第二オクテットを 18 に変更してください。

HOST_NAME=172.17.0.2
              ↓
HOST_NAME=172.18.0.2

AmazonLinux2に構築するコマンド例

$ sudo su
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
# chmod +x /usr/local/bin/docker-compose

# nano /opt/example-service/docker-compose.yml
# cd /opt/example-service/
# /usr/local/bin/docker-compose up -d
# curl http://172.18.0.3/hello

version: '3'
services:
  app:
    image: hello
    networks:
      hello-app-net:
        ipv4_address: 172.18.0.2
    ports:
     - "3031:3031"
    restart: always
  web:
    image: "hello/nginx"
    networks:
      hello-app-net:
        ipv4_address: 172.18.0.3
    ports:
     - "80:80"
    env_file: ./web/env_list
#    depends_on:
#     - app
    restart: always
networks:
  hello-app-net:
    driver: bridge
    ipam:
      driver: default
      config:
      - subnet: 172.18.0.0/24
#        gateway: 172.18.0.1

参考

Flask

https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/

https://flask.palletsprojects.com/en/2.2.x/quickstart/

uWSGI

https://www.python.ambitious-engineer.com/archives/1959

https://uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html

Docker

https://mebee.info/2021/08/05/post-40153/

https://www.tohoho-web.com/docker/docker_logs.html

docker-compose

https://man.plustar.jp/docker/compose/install.html

https://docs.docker.com/compose/compose-file/compose-file-v3/

https://man.plustar.jp/docker/compose/compose-file.html