【Docker】flaskを使ってリバースプロキシを開発しました

プログラミング
スポンサーリンク

スポンサーリンク

初めに

こんにちは、筋トレと将棋が好きな学生エンジニアのゆうき(@engieerblog_Yu)です。

yumでインストールしたファイルが、何らかの理由で欠損してしまった場合、バージョンの互換性が取れなくなってしまう可能性があるという問題があります。

互換性があるバージョンのファイルをキャッシュレポジトリに保存しておくことで、クライアントは以前使っていたファイルを簡単に入手することができます。

今回はディスクにファイルを保存することができるようなリバースプロキシをflaskで開発しました。

またdockerを用いることで、localhostだけでなく、外からのアクセスにも対応しています。

flaskを用いたリバースプロキシの開発

今回開発するリバースプロキシは、クライアントがダウンロードしたいファイルが、リバースプロキシのディスク上にある場合は、Webサーバーに行かずにディスクのファイルを返します。
ディスク上に無い場合は、Webサーバーに取りに行って、ディスクに保存してからクライアントにファイルを返します。
以下実装部分です。

# coding: utf-8
import http.client
import os

from flask import Flask,make_response
from werkzeug.datastructures import Headers

app = Flask(__name__)

@app.route('/repos/<fqdn>/<path:path>')
def change_get_url(fqdn,path):
    h = http.client.HTTPConnection(fqdn)
    h.request('GET', f'/{path}')
    r = h.getresponse()
    out_filepath = "var/repos/{}/{}".format(fqdn,path)
    paths = path.split('/')
    del paths[-1]
    path = "/".join(paths)
    dir_path = "var/repos/{}/{}".format(fqdn, path)

    # ファイルが存在する場合の読み出し
    if os.path.exists(out_filepath):
        with open(out_filepath,mode='rb') as f:
            body = f.read()
            f.close()

    # ファイルが存在しない場合の書き出し
    else:
        # r.read()の内容をファイルに書き出して保存する
        body = r.read()
        # ディレクトリの作成
        os.makedirs(dir_path,exist_ok=True)
        # r.read()の内容をバイナリファイルに書き出して保存する
        with open(out_filepath,mode='wb') as f:
            f.write(body)
            f.close()

    resp = make_response(body)
    d = Headers()
    d.add('Server',r.headers['Server'])
    d.add('Date',r.headers['Date'])
    d.add('Content-Type',r.headers['Content-Type'])
    d.add('Content-Length',r.headers['Content-Length'])
    d.add('Last-Modified',r.headers['Last-Modified'])
    d.add('Connection',r.headers['Connection'])
    d.add('Etag',r.headers['Etag'])
    d.add('Accept-Ranges',r.headers['Accept-Ranges'])
    resp.headers = d
    resp.status = r.status
    return resp


if __name__ == '__main__':
    app.run(debug=False,host='0.0.0.0',port=80)

リバースプロキシを介したrpmファイルのインストール

先ほどのdnf.cache.pyを用いて、rpmファイルをインストールしていきます。
dnf.cache.pyを実行して、サーバーを172.20.10.3:80で立ち上げた状態で、curlコマンドを実行します。

curl -O -v http://172.20.10.3:80/repos/repo.almalinux.org/vault/8/AppStream/debug/x86_64/Packages/389-ds-base-debuginfo-1.4.3.28-6.module_el8.6.0%2B2734%2B1efaf02b.x86_64.rpm

dnf_cacheにrpmファイルを保存することができていることが分かります。

リバースプロキシを介さずに、rpmファイルをダウンロードした場合とファイルが異なっていないか確かめてみます。
先ほどダウンロードしたrpmファイルに.backをつけます。

mv 389-ds-base-debuginfo-1.4.3.28-6.module_el8.6.0%2B2734%2B1efaf02b.x86_64.rpm 389-ds-base-debuginfo-1.4.3.28-6.module_el8.6.0%2B2734%2B1efaf02b.x86_64.rpm.back

次に本物のWebサーバーからrpmファイルをダウンロードします。

curl -O -v http://repo.almalinux.org/vault/8/AppStream/debug/x86_64/Packages/389-ds-base-debuginfo-1.4.3.28-6.module_el8.6.0%2B2734%2B1efaf02b.x86_64.rpm

cmpコマンドでファイルが一致しているか比較してみます。

cmp 389-ds-base-debuginfo-1.4.3.28-6.module_el8.6.0%2B2734%2B1efaf02b.x86_64.rpm 389-ds-base-debuginfo-1.4.3.28-6.module_el8.6.0%2B2734%2B1efaf02b.x86_64.rpm.back

何も表示されなければ、リバースプロキシを介してダウンロードしたものと、本物のWebサーバーからダウンロードしたrpmファイルが一致しているということになります。

Docker上での実行

それでは、localhostだけでなく外からのアクセスに対応するために、Docker上で先ほどのプログラムを動かせるようにしていきます。
まずはDockerイメージを作るためにDockerfileを作成します。

FROM python:3.9-alpine
COPY dnf_cache.py dnf_cache.py
RUN pip install --upgrade pip && pip install flask
ENTRYPOINT ["/usr/local/bin/python",  "dnf_cache.py"]

先ほどのDockerfileで、dockerイメージを作成します。

docker build -t dnf_cache .

コンテナ間通信のために、docker-networkを作成します。

docker network create dnf_cache_net

dnf_cacheという名前で、コンテナを起動します。
networkは先ほど作成した、dnf_cache_netとしています。

docker run -d --name dnf_cache --network dnf_cache_net dnf_cache

almalinuxコンテナを、dnf_cache_netで立ち上げます。

docker run -it --network dnf_cache_net --rm almalinux /bin/bash

dnf_cache_net上でコンテナ間通信ができているかは、以下で確認できます。

docker network inspect dnf_cache_net

dnf_cache_netに二つのコンテナが含まれていることが分かります。

[
    {
        "Name": "dnf_cache_net",
        "Id": "a7ba7dd44340f4553fbeddc9d4e8c121b2f448033c84dac8c341d71abb5b7eb3",
        "Created": "2022-11-05T06:38:57.619638273Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.21.0.0/16",
                    "Gateway": "172.21.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "5c32fd260145c37a1e9da61e7efb6e723f853560d7653d40f68f48e5564fdd9b": {
                "Name": "dnf_cache",
                "EndpointID": "e741a69003cad4aacbb4cbbcd753d1bb6aacd3b422e8380e5a288f4d469ba506",
                "MacAddress": "02:42:ac:15:00:02",
                "IPv4Address": "172.21.0.2/16",
                "IPv6Address": ""
            },
            "f02458eb74e091ddf9ace7f771133ba352500b4a2ced3466d69e2e6497bf9e30": {
                "Name": "dreamy_stonebraker",
                "EndpointID": "8aafa7e554adb97c67c48ce93bf72e2edc92ef02d6fd0acfc861ec7096e11b8f",
                "MacAddress": "02:42:ac:15:00:03",
                "IPv4Address": "172.21.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

almalinuxコンテナ上で、以下を実行します。

sed -i -e 's/^mirrorlist=https:\/\//# mirrorlist=https:\/\//' -e 's/^# baseurl=https:\/\//baseurl=http:\/\/dnf_cache\/repos\//' /etc/yum.repos.d/*.repo
dnf install python39

以下のように表示されれば、コンテナ上でリバースプロキシを介して、pythonをインストールすることができています。

python39 --version
Python 3.9.12

環境変数の設定

最後にクライアントがリバースプロキシを使いたくない場合も想定して、リバースプロキシを使うか否かを環境変数で設定できるようにします。

# coding: utf-8
import http.client
import os

from flask import Flask,make_response
from werkzeug.datastructures import Headers

app = Flask(__name__)

@app.route('/repos/<fqdn>/<path:path>')
def change_get_url(fqdn,path):
    h = http.client.HTTPConnection(fqdn)
    h.request('GET', f'/{path}')
    r = h.getresponse()
    out_filepath = "var/repos/{}/{}".format(fqdn,path)
    paths = path.split('/')
    del paths[-1]
    path = "/".join(paths)
    dir_path = "var/repos/{}/{}".format(fqdn, path)
    if os.environ['CACHE_ONLY'] == 'true':
        # ファイルが存在する場合の読み出し
        if os.path.exists(out_filepath):
            with open(out_filepath,mode='rb') as f:
                body = f.read()
                f.close()

        # ファイルが存在しない場合の書き出し
        else:
            # r.read()の内容をファイルに書き出して保存する
            body = r.read()
            # ディレクトリの作成
            os.makedirs(dir_path,exist_ok=True)
            # r.read()の内容をバイナリファイルに書き出して保存する
            with open(out_filepath,mode='wb') as f:
                f.write(body)
                f.close()

    elif os.environ['CACHE_ONLY'] == 'false':
        # r.read()の内容をファイルに書き出して保存する
        body = r.read()
        # ディレクトリの作成
        os.makedirs(dir_path, exist_ok=True)
        # r.read()の内容をバイナリファイルに書き出して保存する
        with open(out_filepath, mode='wb') as f:
            f.write(body)
            f.close()

    else:
        print("Set environment variables.")
        return

    resp = make_response(body)
    d = Headers()
    d.add('Server',r.headers['Server'])
    d.add('Date',r.headers['Date'])
    d.add('Content-Type',r.headers['Content-Type'])
    d.add('Content-Length',r.headers['Content-Length'])
    d.add('Last-Modified',r.headers['Last-Modified'])
    d.add('Connection',r.headers['Connection'])
    d.add('Etag',r.headers['Etag'])
    d.add('Accept-Ranges',r.headers['Accept-Ranges'])
    resp.headers = d
    resp.status = r.status
    return resp


if __name__ == '__main__':
    app.run(debug=False,host='0.0.0.0',port=80)

以下のコマンドで、dnf_cacheコンテナを立ち上げるときに、リバースプロキシを使用するか否かを指定することができるようになりました。

docker run -e CACHE_ONLY=true -d --name dnf_cache --network dnf_cache_net dnf_cache

先ほどと同様に実行すれば、リバースプロキシを介してpythonがインストールできることが確認できると思います。

ゆうき
ゆうき

最後まで読んでいただきありがとうございました。

コメント

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