リクエストヘッダーを追加するだけの簡易なAPIゲートウェイを作る

サンプルAPIへリクエストする時にリクエストヘッダー１件を追加するだけの、簡易APIゲートウェイを自作してみる。

# 環境
- macOS Ventura
- Rancher Desktop 1.7.0 （Container Engine設定はdockerd）
- Python3, Flask 2.2

# サンプルAPIを作る

ディレクトリ
```
.
├── docker-compose.yml
└── helloapp
    ├── Dockerfile
    ├── app.py
    └── requirements.txt
```

ファイル

```yaml:./docker-compose.yml
version: "3.0"
services:
  helloapp:
    build:
      context: helloapp
      target: builder
    stop_signal: SIGINT
    volumes:
      - ./helloapp:/app
    ports:
      - '10600:5000'
```

```Dockerfile:./helloapp/Dockerfile
# syntax=docker/dockerfile:1.4
FROM --platform=$BUILDPLATFORM python:3.11-alpine AS builder

WORKDIR /app

COPY requirements.txt /app
RUN --mount=type=cache,target=/root/.cache/pip \
    pip3 install -r requirements.txt

COPY . /app

CMD ["flask", "--app", "app.py", "--debug", "run", "--host", "0.0.0.0", "--port", "5000"]

# 調査用
# CMD ["tail", "-f"]
# docker exec -it <container id or name> /bin/sh
# flask --version
```

```:./helloapp/.requirements
flask==2.2.3
```

```python:./helloapp/app.py
from flask import Flask, request, Response
import json
import random

app = Flask(__name__)

@app.route('/<path:path>', methods=['GET', 'POST'])
def hello(path):
	params = {}
	if request.method == 'GET':
		params = request.args
	elif request.method == 'POST':
		params = request.form
	
	j = json.dumps({
		'message': 'Hello world',
			'method': request.method,
			'path': path,
			'params': params,
			'headers': request.headers.to_wsgi_list(), 
	})

	s = random.randrange(400, 405, 1)
	
	return Response(response=j, status=s)

if __name__ == '__main__':
	app.run(host='0.0.0.0', port=8000)
```

```:./helloapp/.dockerignore
__pycache__
```

動作確認（１）
- 上記のファイルを配置して `docker-compose up --build` する
- ホストOSのブラウザなどで http://localhost:10600/foo/bar/hoge?param1=111&param2=222 を開く。レスポンスの例は下記。
```
{"message": "Hello world", "method": "GET", "path": "foo/bar/hoge", "params": {"param1": "111", "param2": "222"}, "headers": [["Host", "localhost:10600"], ["Upgrade-Insecure-Requests", "1"], ["Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"], ["User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15"], ["Accept-Language", "ja"], ["Accept-Encoding", "gzip, deflate"], ["Connection", "keep-alive"]]}
```

# 簡易APIゲートウェイを作る

- Flaskでgatewayを作る
- gatewayはリクエストヘッダー `Additional-Header`を追加して、helloapp のAPIにリクエストする
- gatewayはhelloappのレスポンスを転記して、レスポンスする

ディレクトリ

```diff
  .
  ├── README.txt
  ├── docker-compose.yml
+ ├── gateway
+ │   ├── Dockerfile
+ │   ├── app.py
+ │   └── requirements.txt
  └── helloapp
      ├── Dockerfile
      ├── app.py
      └── requirements.txt
```


ファイル


docker-compose.yml を修正して　 `services.gateway`を追加。
```yaml:./docker-compose.yml
version: "3.0"
services:
  helloapp:
    build:
      context: helloapp
      target: builder
    stop_signal: SIGINT
    volumes:
      - ./helloapp:/app
    ports:
      - '10600:5000'
        # ホストの http://localhost:10600/any-path
  gateway:
    build:
      context: gateway
      target: builder
    stop_signal: SIGINT
    volumes:
      - ./gateway:/app
    ports:
      - '10601:5000'
        # ホストの http://localhost:10601/any-path
```

```Dockerfile:./gateway/Dockerfile
# syntax=docker/dockerfile:1.4
FROM --platform=$BUILDPLATFORM python:3.11-alpine AS builder

WORKDIR /app

COPY requirements.txt /app
RUN --mount=type=cache,target=/root/.cache/pip \
    pip3 install -r requirements.txt

COPY . /app

CMD ["flask", "--app", "app.py", "--debug", "run", "--host", "0.0.0.0", "--port", "5000"]

# 調査用
# CMD ["tail", "-f"]
# docker exec -it <container id or name> /bin/sh
# flask --version
```

```:./gateway/.requirements
flask==2.2.3
requests
```

```python:./gateway/app.py
from flask import Flask, request, Response
import requests

app = Flask(__name__)

@app.route('/<path:path>', methods=['GET', 'POST'])
def hello(path):
	# url
	url = 'http://host.docker.internal:10600/' + path
	
	# headers: copy
	reqHeaders = {}
	for k in request.headers.keys():
		reqHeaders[k] = request.headers[k]
	
	# headers: add
	reqHeaders["additional-header"] = "additional-header-value"

	# params/data
	params = {}
	if request.method == 'GET':
		params = request.args
		res = requests.get(url, params=params, headers=reqHeaders)
	elif request.method == 'POST':
		data = request.form
		res = requests.post(url, data=data, headers=reqHeaders)

	resHeaders = {}
	for k in res.headers.keys():
		resHeaders[k] = res.headers[k]
	
	return Response(response=res.text, headers=resHeaders, status=res.status_code)

if __name__ == '__main__':
	app.run(host='0.0.0.0', port=8000)
```

```:./gateway/.dockerignore
__pycache__
```


動作確認（２）
- 上記のファイルを配置して `docker-compose up --build` する
- ホストOSのブラウザなどで http://localhost:10601/foo/bar/hoge?param1=111&param2=222 を開く。
- ホストOSのzshなどで、コマンドを実行する `curl -X POST --data-urlencode 'name=太郎' -d 'age=30' http://localhost:10601/ababab/beef`
```:ブラウザでアクセスした結果の例
{"message": "Hello world", "method": "GET", "path": "foo/bar/hoge", "params": {"param1": "111", "param2": "222"}, "headers": [["User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Safari/605.1.15"], ["Accept-Encoding", "gzip, deflate"], ["Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"], ["Connection", "keep-alive"], ["Host", "localhost:10601"], ["Upgrade-Insecure-Requests", "1"], ["Accept-Language", "ja"], ["Additional-Header", "additional-header-value"]]}
```

```:curlでPOSTした結果の例
{"message": "Hello world", "method": "POST", "path": "ababab/beef", "params": {"name": "\u592a\u90ce", "age": "30"}, "headers": [["User-Agent", "curl/7.86.0"], ["Accept-Encoding", "gzip, deflate"], ["Accept", "*/*"], ["Connection", "keep-alive"], ["Host", "localhost:10601"], ["Content-Length", "30"], ["Content-Type", "application/x-www-form-urlencoded"], ["Additional-Header", "additional-header-value"]]}%                          
```

# 参考リンク

docker/awesome-compose/flask
https://github.com/docker/awesome-compose/tree/master/flask

how to call another webservice api from flask
https://stackoverflow.com/questions/25149493/how-to-call-another-webservice-api-from-flask

(問題) webアプリをdockerで立ててアクセスしたらERR_EMPTY_RESPONSEエラーになった
(原因) containerの外からリクエストが来るのにアプリがlocalhostでLISTENしている
(解決) アプリの設定を0.0.0.0でLISTENするよう変更する
https://qiita.com/amuyikam/items/01a8c16e3ddbcc734a46

Auto reloading python Flask app upon code changes
https://stackoverflow.com/a/40150705

PythonのFlaskで階層自由なURLをルーティングする方法
https://lightgauge.net/language/python/flask-free-route-path

Docker コンテナ内からホストのポートにアクセスする方法まとめ
host.docker.internal
https://gotohayato.com/content/561/

python requests post リクエスト送信時に、header を設定する
https://www.monotalk.xyz/blog/python-requests-post-リクエスト送信時にheader-を設定する/

FlaskでシンプルにContent-Typeをチェックする（@content_type）
https://qiita.com/5zm/items/c3f004291a87cdbce0b9

How do I set response headers in Flask?
https://stackoverflow.com/questions/25860304/how-do-i-set-response-headers-in-flask

flask Response
https://flask.palletsprojects.com/en/2.2.x/api/#flask.Response

flaskでhttpステータスを返却する方法
https://qiita.com/mink0212/items/52e0ebd66bd94e1303c1

The Request Object
https://flask.palletsprojects.com/en/2.2.x/quickstart/#the-request-object

curlコマンドでPOSTする, 様々な形式別メモ
https://weblabo.oscasierra.net/curl-post/

request.get()
https://www.w3schools.com/python/ref_requests_get.asp
requests.Response Object
https://www.w3schools.com/python/ref_requests_response.asp