flaskを用いたリバースプロキシを使ってyumのキャッシュレポジトリを開発してみる

# はじめに
<!-- 発端や概要を記載 -->
yumでインストールしたファイルが、何らかの理由で欠損してしまった場合、バージョンの互換性が取れなくなってしまう可能性があるという問題があります。

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

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

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



# 目次
<!-- タイトルとアンカー名を編集 -->
1. [使用したライブラリ](#lib)
1. [flaskを用いたリバースプロキシの開発](#Chapter1)
1. [リバースプロキシを介したrpmファイルのインストール](#Chapter2)
1. [環境変数の設定](#Chapter3)
1. [参考文献](#reference)

<!-- 各チャプター -->

# 実行環境
MacOS 12.3.1
<a id="#lib"></a>
# 使用したライブラリ
|lib |    version  |  
|---:|-------------|
| pip |22.2.2|   
| Flask |2.1.3|            
| Python |3.9.12|            

flaskは以下のコマンドでインストールできます。
```
pip install flask
```



<!-- 各チャプター -->
<a id="#Chapter1"></a>
# 1. flaskを用いたリバースプロキシの開発
今回開発するリバースプロキシは、クライアントがダウンロードしたいファイルが、リバースプロキシのディスク上にある場合は、Webサーバーに行かずにディスクのファイルを返します。
ディスク上に無い場合は、Webサーバーに取りに行って、ディスクに保存してからクライアントにファイルを返します。
以下実装部分です。

```dnf_cache.py
# 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)
```

<a id="#Chapter2"></a>
# 2. リバースプロキシを介した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ファイルを保存することができていることが分かります。

![スクリーンショット 2022-11-05 14.35.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2712955/9b27f2f8-fd20-6332-59dc-b35d6e0b77d9.png)
リバースプロキシを介さずに、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ファイルが一致しているということになります。
<a id="#Chapter3"></a>
# 3. Docker上での実行
それでは、localhostだけでなく外からのアクセスに対応するために、Docker上で先ほどのプログラムを動かせるようにしていきます。
まずはDockerイメージを作るためにDockerfileを作成します。
```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
```


<a id="#Chapter4"></a>
# 4. 環境変数の設定
最後にクライアントがリバースプロキシを使いたくない場合も想定して、リバースプロキシを使うか否かを環境変数で設定できるようにします。

```dnf.cache.py
# 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がインストールできることが確認できると思います。






<a id="#reference"></a>
# 参考文献
- [Docker入門（第五回）〜コンテナ間通信〜](https://knowledge.sakura.ad.jp/16082/)
- [HTTPプロトコルクライアント](https://docs.python.org/ja/3.9/library/http.client.html)
- [Flaskを用いたWebアプリケーション開発（HelloWorld）](https://serip39.hatenablog.com/entry/2020/07/05/070000)