【Docker】NGINXリバースプロキシにてバックエンドコンテナへのアクセスを実施する

DockerでFlaskとNGINX連携 Docker
DockerでFlaskとNGINX連携

はじめに

こんにちは。Shoheiです。

SPAとAPIサーバでコンテナ上にWebアプリケーションを開発する機会があったので、フロントコンテナのリバースプロキシによるバックエンドコンテナアクセスを可能にする構成について備忘録として残します。

今回やりたいこと

さて、本題に入りましょうか。
今回は、Dockerコンテナ環境で、Dockerのブリッジネットワーク内にNGINX⇄uWSGI⇄Flaskの環境を構築し、バックエンドに対する外部からのアクセスはNGINXを介したプロキシで実施する構成について残していこうと思います。

具体的な実現したいゴール

  • http://<Target_IP>:8080/ にアクセスし、「Hello NGINX!」を取得
  • http://<Target_IP>:8080/api/hello にアクセスし、NGINXを経由してバックエンドで5000番ポートで提供しているAPIからの返却値「Backend」を取得

得られる知見

* NGINXの簡単なプロキシ設定
* uWSGIによるFlask起動方法
* 上記構成のDocker-Compose.yaml、Dockerfileの参考例

個人的にこの構成は、SPAにおけるフロントとバックエンドAPIサーバ間でのやり取りにおいて使うことがあったりするのかなと思っています。
AWSのFargateを利用した開発なども増えているので、その前知識としてはいいかも。。

個人的難易度

インフラ:★★★☆☆☆☆☆☆☆
アプリケーション:★☆☆☆☆☆☆☆☆☆

必要知識

  • コンテナの基本知識
  • NW(TCP/IP)の基本知識
  • Webアプリケーションの基本知識

検証

検証環境

* AWS EC2(Amazon Linux2)
* Docker
* NGINX(Webサーバ)
* Python 3.9.10
* Flask(Pythonベースのアプリケーションサーバ)
* uWSGI(規格統合インターフェース)

※留意点

今回は、AWS環境を想定しているので、NGINXのみをアクセスポイントとする構成のための制限はAWSのセキュリティグループなど、インフラ周りで制御する方針を考えているため、DockerネットワークやOSレベルでの通信制限は検討外です。
また、今回はNGINX⇄uWSGI間の通信はHTTPとします。
Docker環境構築に関する内容は本質事項ではないため省略します。

開発ディレクトリ

¥~/docker-test
    ├── /app
    │       ├── Dockerfile
    │       ├── app.py
    │       ├── wsgi.py
    │       └── req.txt
    ├── /web
    │       ├── Dockerfile
    │       ├── index.html
    │       ├── nginx.conf
    │       └── default.conf
    └── docker-compose.yaml


Flask APIサーバの準備

Pythonライブラリインストール(req.txt)

今回、バックエンドコンテナ内では下記のライブラリをインストールします。
後ほどDockerfile内でも説明しますが、下記のようなインストールライブラリを記載したファイルを読み込むには

pip install -r req.txt

上記コマンドを実行する形になります。

Flask==2.0.2
Flask==2.0.20
FLASK-CORS==3.0.10

APIの実装

今回は簡易的なAPIサーバを構築します。
今回はFlaskに関して詳しい説明は省略しますが、CORSとはCross Origin Resource Sharingという異なるオリジン間でのリソース共有の設定になります。
この設定は端的にいうと悪意のある攻撃者からの攻撃を防ぐ設定で、異なるオリジン(≒ドメイン: google.comなど)の間で画像やAPIリソースへのアクセスを共有するための設定で、特に今回はAPIを提供するバックエンド側での設定をすることで、NGINXで提供するフロントエンドサーバからのAPIアクセスを可能にしています。

【app.py】

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app, supports_credentials=True)

@app.route('/api/hello', methods=["GET"])
def get_hello():
    return 'Hello Backend!'

uWSGIからの起動準備

今回は自分の学習も兼ねて、Flask内蔵のwerkzeugサーバではなく、マルチリクエストなどにも対応できる商用向けWSGIサーバであるuWSGIを利用します。
※WSGIサーバについては別途記事化する予定ですが、WSGIサーバにはuWSGI以外にもGunicornなど他にもあります。

まずuWSGIの起動設定ファイルであるapi.iniファイルを実装します。

moduleとはwsgiから実行するFlaskモジュールのことで、先ほどのapp.pyで実装したappモジュールを指定します。(実際には後ほど実装するwsgi.pyで読み込んでいるappモジュールを実行します。)
wsgi-fileとはwsgiからFlaskを実行するインターフェースのような役割を持っているファイルを指定します。

【api.ini】

[uwsgi]
module = app
callable = app
wsgi-file = /app/wsgi.py
http = 0.0.0.0:5000

続いて、先ほど少し話題に出したwsgi経由で起動するためのインターフェースであるwsgi.pyを実装します。

【wsgi.py】

from app import app

if __name__ == '__main__':
    app.run()

NGINXの準備

NGINXではindex.htmlとNGINXの設定ファイルであるdefault.confの2つのファイルを用意します。

まずはindex.htmlです。
今回はCDN形式でVue.js3を読み込んで、簡易的なWebアプリを作ります。

ゴール感としては、何もしないと「Welcome to NGINX」を表示されていて、ボタンを押すとバックエンドにAPIアクセスに行き、レスポンスとして「Backend」文字列を返されるので、表示を「Welcome to Backend」と変更する動作を実現することです。

【index.html】

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <p>Welcome to {{msg}}</p>
        <button @click="accessApp">Access to Backend Container</button>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
    new Vue({
        el: "#app",
        data: {
            msg: "NGINX"
        },
        methods: {
            accessApp(){
                axios.get('http://127.0.0.1:8080/api/hello')
                    .then(response => (this.msg = response.data))
            }
        },
    })
</script>
</html>

Vue.jsに関しては今回は詳しく触れませんが、動きとしてはAccess to Backend Containerボタンを押下すると、axiosを通してバックエンドのAPI(http://<Global-IP-address-of-server>:8080/api/hello)にアクセスしに行く流れとなります。
後ほど説明しますが、NGINXコンテナは8080番ポート、バックエンドは5000番ポートで起動するため、直接バックエンドの5000番ポートではなく、フロントエンドの8080番ポートにアクセスしに行っていることがわかるかと思います。

続いてNGINXの設定ファイルであるdefault.confの実装に移ります。

リバースプロキシの設定をdefault.confで実施します。

【default.conf】

server {
    listen 80;
    server_name test;

    location / {
        root "/usr/share/nginx/html";
        index index.html;
    }
    location /api/ {
        proxy_pass http://backend:5000;
    }
}

さて中身を見ていきましょう。
locationがルート(/)の時はindex.htmlへのアクセス設定となっています。

一方で、ロケーションが/api/を含むときはhttp://backend:5000へプロキシしていることがわかります。
この時、2つのポイントがあります。

  • http://backend:5000のbackendドメインはどこからきたか。

backendドメインはどこで登録しているかというと、dockerコンテナを起動するときにバックエンドコンテナのコンテナ名を「backend」としています。
そうすることにより、Dockerの同一ブリッジネットワーク内であれば独自の名前解決機能が働き、コンテナ名をドメイン登録しているため、http://backendでバックエンドコンテナに到達します。

  • proxy_pass http://backend:5000/; ではなく、proxy_pass http://backend:5000; である。

ここは少しややこしいのですが、こちらのサイトが参考になります。
locationの文字列をプロキシのアクセスパスとして残すか残さないか、の違いになります。
今回の記載の場合、省略をしないダイレクトな記載になります。

Docker構築

Dockerfileの準備

今回、NGINXはDockerHubから直接イメージを引っ張ってきて、そこに必要なファイルを導入していく形式を取るため、不ロントエンドコンテナ用にはDockerfileを用意しません。

一方でバックエンドコンテナに関しては、PythonのイメージをDockerHubから取得し、そこに対してFlask、uWSGI環境を構築するためカスタムが必要になり、Dockerfileを作成します。
なのでここではバックエンドコンテナに関するDockerfileの説明になります。

【Dockerfile】

FROM python:3.9.10

WORKDIR /app
COPY ./req.txt .
COPY ./api.ini .
RUN pip install --upgrade pip
RUN pip install -r /app/req.txt

メインでPython環境の構築のためにライブラリのインストールを実施しています。

docker-compose.yamlの準備

それでは次にdocker-composeの準備をしていきましょう。

【docker-compose.yaml】

version: '3'

services:
    frontend:
        image: nginx
        ports:
        - 8080:80
        volumes:
        - ./web/default.conf:/etc/nginx/conf.d/default.conf
        - ./web/index.html:/usr/share/nginx/html/index.html
        restart: always
    backend:
        build: ./app
        volumes:
        - ./app:/app/
        ports:
        - 5000:5000
        command: uwsgi --ini /app/api.ini

フロントエンドはブリッジネットワーク外では8080番ポートで紐付け、内部では80番ポートで起動しています。
index.hemlとdefault.confはホストサーバ上でvolumeリンクをとっており、コンテナ内には実施にファイルを移しているわけではありません。

バックエンドはブリッジネットワーク内外とも5000番ポートで紐づけて起動しています。
フロントエンド同様にapp/配下をvolumeリンクしています。
最後にuWSGI起動コマンドを注入してコンテナを起動しています。

先ほどの「http://backend:5000のbackendドメインはどこからきたか。」というポイントに関して、backendコンテナのドメイン解決を可能にするためのコンテナ名の指定はservices配下のbackendと指定している箇所になります。

Docker起動

それでは実際にDockerを起動していきましょう。

docker-compose.yamlファイルがあるディレクトリ上で下記コマンドを実行します。

$ docker-compose -f docker-compose.yaml build
<省略>

$ docker-compose -f docker-compose.yaml up -d
<省略>

$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
24ada477a88a nginx "/docker-entrypoint.…" 47 minutes ago Up 32 minutes 0.0.0.0:8080->80/tcp nginx-flask-test-frontend-1
7a258e24107d nginx-flask-test_backend "uwsgi --ini /app/ap…" 55 minutes ago Up 32 minutes 0.0.0.0:5000->5000/tcp nginx-flask-test-backend-1

確認

それでは実際にブラウザ上から確認していきましょう。

今回はMac自身をホストとしてDockerを起動しているので、アクセス先は「http://localhost:8080」となります。

実際にアクセスした画面が下記となります。

続いて「Access to Backend Container」ボタンを押下した後の画面が下記となります。

「Welcome to NGINX」が「Welcome to Backend」に変わっていることが確認できます。

これにより、これまで実装してきた設定で正常にブリッジネットワーク内にてNGINXのリバースプロキシを通して外部からバックエンドにアクセスできていることを確認できました。

もちろん今回はアクセス制御やACLなどの設定をしていないため、「http://localhost:5000/api/hello」にアクセスすることでダイレクトにバックエンドコンテナにアクセスさせることも可能です。


おわりに

いかがでしたでしょうか。

今回はDockerコンテナによる、フロントエンドコンテナとバクエンドコンテナの構築、フロントエンドのリバースプロキシを使ったバックエンドへのアクセス検証を実施しました。

これらを応用してコンテナ間の通信やマイクロアプリケーションの構築が発展的にできるかと思いますので、ぜひ参考にしてください。

それでは良いコンテナライフを!!

タイトルとURLをコピーしました