Elasticsearch Python Client (elasticsearch-py)でProxyを設定する方法

# 概要
Elasticsearch Python Client (elasticsearch-py)をプロキシサーバを通して利用する方法がググってもあまり出てこないため、簡単に設定方法をまとめました。
elasticsearch-pyのバージョンによって設定方法が少し異なりますので、v7とv8で分けて記載します。
[elasticsearch-pyのドキュメント](https://www.elastic.co/guide/en/elasticsearch/client/python-api/8.5/index.html)
[elasticsearch-pyのGitHub](https://github.com/elastic/elasticsearch-py)
# 準備
お手軽に試せるようにdocker-compose一発で環境が整うようにします。
## Elastic Cloud
[こちらを参考に作成](https://qiita.com/tomo_s_el/items/3584d0b1fabb0bafa4fa)
## 構成
以下のようなファイルを用意します。
```
.
├── app
│   ├── Dockerfile
│   ├── v7.py
│   └── v8.py
├── docker-compose.yml
└── squid
    └── Dockerfile
```
ではそれぞれのファイルの中身を見てみましょう。
## プロキシサーバ
Docker HubにあるUbuntu/Squidイメージを使用します。
Dockerfileはこちら
```dockerfile
FROM ubuntu/squid
RUN echo acl SSL_ports port 443 9243 >> /etc/squid/squid.conf
```
Elasticsearchのポートが`9243`ですのでこのポートだけ追加をしています。
## Python Client
Docker Hubにあるpython:bullseyeを使用します。
### Dockerfile
Dockerfileはこちら
`apt install`しているパッケージは後ほどプロキシ経由に通信を変更する際に使用します。
#### v8
```dockerfile
FROM python:bullseye
RUN apt update
RUN apt install -y net-tools iputils-ping curl
RUN pip3 install requests　elasticsearch
COPY v8.py v8.py
```
#### v7
version 7でのインストールを明記します。
```dockerfile
FROM python:bullseye
RUN apt update
RUN apt install -y net-tools iputils-ping curl
RUN pip3 install requests elasticsearch==7.17.8
COPY v7.py v7.py
```
### docker-compose
```yaml
version: '3.7'
services:
  app:
    build:
      context: ./app
      dockerfile: Dockerfile
    container_name: app
    command: /bin/sh -c "while :; do sleep 10; done"
    privileged: true
  squid:
    build:
      context: ./squid
      dockerfile: Dockerfile
    container_name: squid
    ports:
      - 3128:3128
```
`command`部分はコンテナが落ちないようにしているだけです。
## ソースコード
### v8
```python
from elasticsearch import Elasticsearch
from elastic_transport import RequestsHttpNode

es = Elasticsearch("https://xxxx.es.asia-northeast1.gcp.cloud.es.io:9243",basic_auth=("elastic","xxxxxxxx"),node_class=RequestsHttpNode)
print(es.info())
```
v8.xはシンプルです。プロキシサーバの情報は環境変数から持ってきます。
### v7
```python
from elasticsearch import Elasticsearch, RequestsHttpConnection

class MyConnection(RequestsHttpConnection):
    def __init__(self,*args, **kwargs):
        proxies = kwargs.pop('proxies', {})
        super(MyConnection, self).__init__(*args, **kwargs)
        self.session.proxies = proxies

es = Elasticsearch("https://xxxx.es.asia-northeast1.gcp.cloud.es.io:9243",http_auth=("elastic","xxxxxxx"),connection_class=MyConnection, proxies = {})
print(es.info())
```
v7.xではconnection classを自前で用意する必要があります。
MyConnectionクラスがそれにあたります。
こちらを参考にしました。
https://github.com/elastic/elasticsearch-py/issues/275
プロキシサーバの情報はv8と同じように環境変数から持ってきます。
# 動作確認
では実際に動かしてみましょう。
## v8
Dockerfileをv8のものにして`docker-compose build` `docker-compose up -d`を実行します。`docker-compose ps`で以下のようになればOKです。
```
Name               Command               State           Ports         
-----------------------------------------------------------------------
app     /bin/sh -c while :; do sle ...   Up                            
squid   entrypoint.sh -f /etc/squi ...   Up      0.0.0.0:3128->3128/tcp
```
では`docker exec -it app /bin/bash`で`app`に入って実際に通信を見てみましょう。
```
# curl google.com
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>

# python v8.py 
{'name': 'instance-0000000001', 'cluster_name': 'e939240763684a8db3183d6f8b47d0a1', 'cluster_uuid': 'eLMujpfhTr2vc92rhENRvA', 'version': {'number': '8.4.2', 'build_flavor': 'default', 'build_type': 'docker', 'build_hash': '89f8c6d8429db93b816403ee75e5c270b43a940a', 'build_date': '2022-09-14T16:26:04.382547801Z', 'build_snapshot': False, 'lucene_version': '9.3.0', 'minimum_wire_compatibility_version': '7.17.0', 'minimum_index_compatibility_version': '7.0.0'}, 'tagline': 'You Know, for Search'}
```
これはもちろんプロキシサーバ経由ではありません。
少し乱暴ですが`route delete default`でルートを潰してみます。
```
# route delete default
# curl google.com
curl: (6) Could not resolve host: google.com
# python v8.py 
Traceback (most recent call last):
  File "//v8.py", line 5, in <module>
    print(es.info())
          ^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/elasticsearch/_sync/client/utils.py", line 414, in wrapped
    return api(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/elasticsearch/_sync/client/__init__.py", line 2296, in info
    return self.perform_request(  # type: ignore[return-value]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/elasticsearch/_sync/client/_base.py", line 286, in perform_request
    meta, resp_body = self.transport.perform_request(
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/elastic_transport/_transport.py", line 329, in perform_request
    meta, raw_data = node.perform_request(
                     ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/elastic_transport/_node/_http_requests.py", line 244, in perform_request
    raise err from None
elastic_transport.ConnectionError: Connection error caused by: ConnectionError(Connection error caused by: ConnectionError(HTTPSConnectionPool(host='dep1.es.asia-northeast1.gcp.cloud.es.io', port=9243): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0xffff8341cdd0>: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution'))))
```
では次にプロキシサーバを環境変数で設定します。
```
export http_proxy=http://squid:3128
export https_proxy=http://squid:3128
```
`curl google.com`も`python v8.py`も問題なく動作します。
ここでSquidログを見てみます。
```
# docker-compose logs
...
squid    | 1671678795.332     92 192.168.0.2 TCP_MISS/301 869 GET http://google.com/ - HIER_DIRECT/172.217.175.46 text/html
squid    | 1671678828.075     72 192.168.0.2 TCP_TUNNEL/200 7151 CONNECT xxxxxxx.es.asia-northeast1.gcp.cloud.es.io:9243 - HIER_DIRECT/34.85.47.11 -
```
無事プロキシサーバ経由で動作していることが確認できます。

## v7
v8と全く同じ手順でテストできます。

# まとめ
ElasticのドキュメントにはPython Clientをプロキシサーバ経由で使用する場合についての記述がありません。
この記事が少しでもお役に立てばと思います。

# おまけ
Advent Calendarが埋まってしまったため、一部掲載できない記事がありました。
弊社のコンサルタントによるKibana VEGAについての記事です。
こちらもぜひご覧ください。
https://gist.github.com/ijokarumawak/0dfcc1cca3ad38e1d5aae3b61b787cd4

### Elastic Cloud 無料トライアル
こちらからElastic Cloudの14日間無料トライアルを是非お試しください
[Elastic Cloud 無料トライアル](https://cloud.elastic.co/)

手順は下記リンクをご参照ください
[Elastic Cloud について　〜実際にデプロイメントを作ってみよう〜](https://qiita.com/tomo_s_el/items/3584d0b1fabb0bafa4fa)