httpbinをk8s環境にデプロイする

# はじめに

[httpbin.org](https://httpbin.org/)はHTTPクライアントをテストするために重宝されているWebサイトです。

頻繁にテストするのはネットワーク資源的な意味合いで無駄だなと感じたので手元の環境にデプロイしました。

ついでに他の人達も利用できるようにURL Prefixを変更できるように改造してcontext-rootを変更可能なアプリケーションにしつつ、パッケージを最新版にして動かしてみました。

オリジナルはコンテナも配布されていますが、だいたい6年ぐらい前でgithubの更新も止っていて、Python3.6ベースになっています。

オリジナルを引き継いでPyplに登録されているバージョンもありますが、今回はオリジナルをforkしています。

## 成果物

* [https://github.com/YasuhiroABE/httpbin](https://github.com/YasuhiroABE/httpbin)
* [https://hub.docker.com/r/yasuhiroabe/my-httpbin/tags](https://hub.docker.com/r/yasuhiroabe/my-httpbin/tags)

## 参考資料

* [https://github.com/postmanlabs/httpbin](https://github.com/postmanlabs/httpbin) オリジナル版httpbin
* [https://github.com/psf/httpbin](https://github.com/psf/httpbin) Pyplに現在登録されている[httpbin](https://pypi.org/project/httpbin/)

## 目標と成果

とりあえず次の点を目指しました。

* 最新版のパッケージ + python:3.12-alpineの最新版コンテナに更新する
* Dockerfileをmulti-stage buildに対応させる
* pipenvを止めてvenv+pipに移行する
* URL Prefixを変更して"/"以外のパスでも動作するようにする
* 組織の中で他の人達が利用できるようにKubernetes上のサービスの1つとして稼動する

オリジナルは6年ほど前で止まっているのでPythonのバージョンを最新版のalpineコンテナで動作させることを目標にしています。

Dockerfileはオリジナルに近いと130MB程度になりますが不要な開発環境を分離するためにMulti-stage buildに移行しました。だいたいコンテナサイズが半分程度になっています。

pipenvはhttpbin.orgと開発者が同じなので思い入れはあると思いますが、最新のパッケージと互換性のない機能を使っていたり、開発環境とコンテナのビルドをほぼ同じ手順で実施するためにシンプルなvenv+pipの構成に変更しました。

URL Prefixを変更可能にするのはKubernetes上で **https://example.com/httpbin/** のような1サービスとして稼動させるためです。

## 主な変更箇所

httpbinの機能には変更を加えないように次のような変更を行いました。

### werkzeugパッケージへの対応

parse_authorization_headerがdeprecatedになっていたため、Authorization.from_headerに変更するための変更を行っています。

この他 BaseResponse型へのアクセスにも問題があったので、import文を修正しました。

```diff:httpbin/helpers.py
diff --git a/httpbin/helpers.py b/httpbin/helpers.py
index b29e183..e93bd14 100644
--- a/httpbin/helpers.py
+++ b/httpbin/helpers.py
@@ -13,7 +13,7 @@ import re
 import time
 import os
 from hashlib import md5, sha256, sha512
-from werkzeug.http import parse_authorization_header
+from werkzeug.datastructures.auth import Authorization
 from werkzeug.datastructures import WWWAuthenticate
 
 from flask import request, make_response
```

### 正規表現やascii-artの表記の変更

raw stringが使われていなかったため、いくつかのstringをraw stringに変更しています。

対応しないと実行時に次のようなエラーになります。

```text:コンテナ実行時のエラーメッセージ
[2024-08-09 06:36:06 +0000] [1] [INFO] Starting gunicorn 22.0.0
[2024-08-09 06:36:06 +0000] [1] [INFO] Listening at: http://0.0.0.0:8080 (1)
[2024-08-09 06:36:06 +0000] [1] [INFO] Using worker: gevent
[2024-08-09 06:36:06 +0000] [2] [INFO] Booting worker with pid: 2
/httpbin/httpbin/helpers.py:26: SyntaxWarning: invalid escape sequence '\_'
  ASCII_ART = """
/httpbin/httpbin/helpers.py:438: SyntaxWarning: invalid escape sequence '\s'
  match = re.search('\s*(W/)?\"?([^"]*)\"?\s*', part)
```

先ほどの httpbin/helpers.py について、次のような変更を行いました。

```diff:raw stringへの変更例
@@ -435,7 +435,7 @@ def parse_multi_value_header(header_str):
     if header_str:
         parts = header_str.split(',')
         for part in parts:
-            match = re.search('\s*(W/)?\"?([^"]*)\"?\s*', part)
+            match = re.search(r'\s*(W/)?\"?([^"]*)\"?\s*', part)
             if match is not None:
                 parsed_parts.append(match.group(2))
     return parsed_parts
```

### URL Prefixの追加への対応

ほとんどの変更は httpbin/core.py に集中しています。

例えば次のように全ての@app.route()にURL Prefixを追加するようにしました。

```diff:
diff --git a/httpbin/core.py b/httpbin/core.py
index 305c988..ad98538 100644
--- a/httpbin/core.py
+++ b/httpbin/core.py
@@ -259,7 +261,7 @@ def view_html_page():
     return render_template("moby.html")
 
 
-@app.route("/robots.txt")
+@app.route(os.environ.get("FLASGGER_URL_PREFIX", "/") + "robots.txt")
 def view_robots_page():
     """Returns some robots.txt rules.
```

この他にstaticファイルを提供するためにstatic_url_pathを追加しています。

```diff:httpbin/core.pyの該当箇所
@@ -82,33 +82,34 @@ BaseResponse.autocorrect_location_header = False
 # Find the correct template folder when running from a different location
 tmpl_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
 
-app = Flask(__name__, template_folder=tmpl_dir)
+app = Flask(__name__, template_folder=tmpl_dir, static_url_path=(os.environ.get("FLASGGER_URL_PREFIX") + "static"))
 app.debug = bool(os.environ.get("DEBUG"))
 app.config["JSONIFY_PRETTYPRINT_REGULAR"] = True
```

FlaskとしてはBlueprintを利用することでurl_prefixでURLを調整する方法がありそうですが、オリジナルがそのような構造になっていなかったので一括置換+手動で修正しました。

よほど単純なサンプル・アプリケーション以外はFlask+Blueprintで開発をするのが良さそうですね。

## 開発者情報などの変更

e-mailアドレスや開発者のホームページといった情報は httpbin/core.py の情報を変更することで実現できます。

今回はこの情報を環境変数を参照して変更できるように変更しました。

```diff:
 template = {
     "swagger": "2.0",
     "info": {
-        "title": "httpbin.org",
+        "title": os.environ.get("HTTPBIND_TITLE", "httpbin.org Compatible API Server"),
         "description": (
-            "A simple HTTP Request & Response Service."
-            "<br/> <br/> <b>Run locally: </b> <code>$ docker run -p 80:80 kennethreitz/httpbin</code>"
+            "A simple HTTP Request & Response Service. The original version is developed by Kenneth Reitz"
+            "<br/> <br/> <b>Run locally: </b> <code>$ podman run -p 8080:8080 -it --rm docker.io/yasuhiroabe/
my-httpbin</code>"
         ),
         "contact": {
             "responsibleOrganization": "Kenneth Reitz",
-            "responsibleDeveloper": "Kenneth Reitz",
-            "email": "me@kennethreitz.org",
-            "url": "https://kennethreitz.org",
+            "responsibleDeveloper": os.environ.get("Yasuhiro ABE", "Kenneth Reitz"),
+            "email": os.environ.get("HTTPBIND_AUTHOR_EMAIL", "me@kennethreitz.org"),
+            "url": os.environ.get("HTTPBIND_AUTHOR_URL", "http://kennethreitz.org/"),
         },
         # "termsOfService": "http://me.com/terms",
-        "version": version,
+        "version": os.environ.get("HTTPBIND_VERSION", version),
     },
```

タイトル以外はオリジナルの開発者の情報をデフォルト値として残しつつ、変更可能となるように変更しました。

## さいごに

この他にpsf/httpbinをチェックして、digest-authが正常に動作しない問題も修正しています。

httpbinは便利で、[https://hc.apache.org/](https://hc.apache.org/)のサンプルコードなどいろいろな場面で利用されています。開発者目線ではローカルで起動すれば十分だとは思うのですが、教育目的ではお手軽にアクセスできるところに置いておきたいツールでもあります。

go-httpbinなど派生ツールもあるので、いろいろ探しつつHTTPを理解するために自作してみるのも良いかもしれません。

以上