Dockerコンテナ上のPythonプログラムからPostgreSQLに接続する


# 初めに
Dockerコンテナ上で動くPythonプログラムを作成してPostgreSQLの接続をしようと思ったら，PostgreSQLのコンテナに接続ができなくてハマったのでその備忘録．

# PostgreSQLのコンテナの作成

まず初めにPostgreSQLと開発するアプリケーションのコンテナを作成する．最終的に，APPコンテナからDBコンテナにPythonプログラムを用いて接続を行い，DBコンテナのデータをAPPコンテナから扱うことができるようにする．．

```:docker-compose.yml

version: "2"
services:
  postgres:
    image: postgres:10
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: postgre
      POSTGRES_PASSWORD: postgre
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=ja_JP.UTF-8"
    volumes:
      - ./db/psgl:/var/lib/postgresql/data
  
  app:
    build: .
    depends_on:
      - postgres
    environment:
        TZ: "Asia/Tokyo"
```

こちらのファイルでPostgreSQLのコンテナを起動して，データベースに接続してみる．

```sh:
$ docker-compose up -d postgres
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                    NAMES
d6aa15a203fc        postgres:10         "docker-entrypoint.s…"   About a minute ago   Up About a minute   0.0.0.0:5432->5432/tcp   dbdocker_postgres_1
# 起動が確認できたので，コンテナにログイン．
$ docker exec -it dbdocker_postgres_1 /bin/bash
oot@d6aa15a203fc:/# psql -U postgre #←docker-composeの環境変数で指定したユーザー名
psql (10.4 (Debian 10.4-2.pgdg90+1))
Type "help" for help.

postgre=# \l
                                 List of databases
   Name    |  Owner   | Encoding |  Collate   |   Ctype    |   Access privileges
-----------+----------+----------+------------+------------+-----------------------
 postgre   | postgres | UTF8     | en_US.utf8 | en_US.utf8 |
 postgres  | postgres | UTF8     | en_US.utf8 | en_US.utf8 |
 template0 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.utf8 | en_US.utf8 | =c/postgres          +
           |          |          |            |            | postgres=CTc/postgres
(4 rows)

postgre=#
```

コンテナ内のPostgreSQLに，`docker-compose.yml`に記述した環境変数のユーザ名のアカウントとデータベースが作成され，作成されたユーザでログインできることが確認できた．


## ローカルからPostgreSQLのコンテナへの接続

次にPythonスクリプトを用いて，ローカルの環境から先ほど作成したPostgreSQLに接続を試みる．
アプリケーション側から接続の意思確認だけしたいので,SQLの情報をプリントするだけの簡単なソースコードで接続確認を行う．

```python:main.py
import os
import psycopg2

DATABASE_URL='postgresql://postgre:postgre@localhost:5432/postgres'

def main():
    cursor = psycopg2.connect(DATABASE_URL)
    print(cursor)

if __name__ == '__main__':
    main()
```

こちらを実行すると接続に成功した場合，以下のようにデータベースのステータスが表示される．

```sh:
$ python main.py
<connection object at 0x10bd09e60; dsn: 'user=postgre password=xxx dbname=postgres host=localhost port=5432', closed: 0>
$
```


## コンテナからPostgreSQLのコンテナへの接続

上の`main.py`をコンテナ上から起動をして，APPコンテナからDBコンテナへの意思疎通を図ってみる．以下の`Dockerfile`を用いてPostgreSQLに接続を行うAPPコンテナを作成する．

```Dockerfile:Dockerfile
FROM python:latest

# pythonライブラリのインストール
RUN apt-get update
RUN pip install --upgrade pip

RUN pip install psycopg2

# プロジェクト追加
ADD main.py .

CMD [ "python", "./main.py" ]

```

先の`docker-compose.yml`と合わせて以下のコマンドで実行．

```sh:
$ docker-compose up -d postgres app
Creating dbdocker_postgres_1 ... done
Creating dbdocker_app_1      ... done
#　起動できたかログを確認
$ docker-compose logs
Attaching to dbdocker_app_1, dbdocker_postgres_1
postgres_1  | 2018-08-24 09:14:30.228 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
postgres_1  | 2018-08-24 09:14:30.228 UTC [1] LOG:  listening on IPv6 address "::", port 5432
postgres_1  | 2018-08-24 09:14:30.232 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
postgres_1  | 2018-08-24 09:14:30.332 UTC [25] LOG:  database system was shut down at 2018-08-24 09:14:21 UTC
postgres_1  | 2018-08-24 09:14:30.364 UTC [1] LOG:  database system is ready to accept connections
app_1       | /usr/local/lib/python3.6/site-packages/psycopg2/__init__.py:144: UserWarning: The psycopg2 wheel package will be renamed from release 2.8; in order to keep installing from binary please use "pip
 install psycopg2-binary" instead. For details see: <http://initd.org/psycopg/docs/install.html#binary-install-from-pypi>.
app_1       |   """)
app_1       | Traceback (most recent call last):
app_1       |   File "./main.py", line 11, in <module>
app_1       |     main()
app_1       |   File "./main.py", line 7, in main
app_1       |     cursor = psycopg2.connect(DATABASE_URL)
app_1       |   File "/usr/local/lib/python3.6/site-packages/psycopg2/__init__.py", line 130, in connect
app_1       |     conn = _connect(dsn, connection_factory=connection_factory, **kwasync)
app_1       | psycopg2.OperationalError: could not connect to server: Connection refused
app_1       |   Is the server running on host "localhost" (127.0.0.1) and accepting
app_1       |   TCP/IP connections on port 5432?
app_1       | could not connect to server: Cannot assign requested address
app_1       |   Is the server running on host "localhost" (::1) and accepting
app_1       |   TCP/IP connections on port 5432?
app_1       |
```

コネクションエラーでた．

### コンテナ間のネットワークの指定方法によるエラー
ローカルホストのポート5432が空いてないということでエラーを返された．`main.py`では`DATABASE_URL`に`app_1`のコンテナのlocalhostを指定しているため起きたエラー．
ユーザから見たらDBコンテナはlocalhostだが，APPコンテナから見るとDBコンテナはlocalではない．コンテナから別のコンテナを指定するときは，コンテナの名前が接続可能なアドレスになるらしい．なので`main.py`の`DATABASE_URL`を以下のように変更する．


```python:main.py
import os
import psycopg2

# DATABASE_URL='postgresql://postgre:postgre@localhost:5432/postgres'
DATABASE_URL='postgresql://postgre:postgre@dbdocker_postgres_1:5432/postgres'

def main():
    cursor = psycopg2.connect(DATABASE_URL)
    print(cursor)

if __name__ == '__main__':
    main()

```

参考にさせていただいたもの
>[Docker の基本学習 ~ コンテナ間のリンク](https://qiita.com/Arturias/items/75828479c1f9eb8d43fa)
>[Docker Composerではコンテナ間通信はコンテナ名を指定する](https://qiita.com/Quantum/items/7e6e3e7a3bdf605c306a)

修正をしたので，もう一度起動して見る．

```sh:
$ docker-compose up -d postgres app
Creating dbdocker_postgres_1 ... done
Creating dbdocker_app_1      ... done
$ docker-compose logs
Attaching to dbdocker_app_1, dbdocker_postgres_1
postgres_1  | 2018-08-24 09:14:30.228 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
postgres_1  | 2018-08-24 09:14:30.228 UTC [1] LOG:  listening on IPv6 address "::", port 5432
postgres_1  | 2018-08-24 09:14:30.232 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
postgres_1  | 2018-08-24 09:14:30.332 UTC [25] LOG:  database system was shut down at 2018-08-24 09:14:21 UTC
postgres_1  | 2018-08-24 09:14:30.364 UTC [1] LOG:  database system is ready to accept connections
app_1       | /usr/local/lib/python3.6/site-packages/psycopg2/__init__.py:144: UserWarning: The psycopg2 wheel package will be renamed from release 2.8; in order to keep installing from binary please use "pip
 install psycopg2-binary" instead. For details see: <http://initd.org/psycopg/docs/install.html#binary-install-from-pypi>.
app_1       |   """)
app_1       | Traceback (most recent call last):
app_1       |   File "./main.py", line 11, in <module>
app_1       |     main()
app_1       |   File "./main.py", line 7, in main
app_1       |     cursor = psycopg2.connect(DATABASE_URL)
app_1       |   File "/usr/local/lib/python3.6/site-packages/psycopg2/__init__.py", line 130, in connect
app_1       |     conn = _connect(dsn, connection_factory=connection_factory, **kwasync)
app_1       | psycopg2.OperationalError: could not connect to server: Connection refused
app_1       |   Is the server running on host "dbdocker_postgres_1" (172.19.0.2) and accepting
app_1       |   TCP/IP connections on port 5432?
app_1       |
$
```

コネクションエラー出た(2回目)．

### コンテナの起動待ちによる接続エラー
どうやらこれはDBコンテナの起動が完了する前に，`main.py`でデータベースに接続しようとしたために起きるエラーらしい．解決するには，DBコンテナが起動した後にAPPコンテナを実行するように，処理順を定義する必要がある．
そこで今回は[こちら](https://stackoverflow.com/questions/47266495/can-not-connect-to-the-container-based-on-postgres)
を参考に，`dadarek/wait-for-dependencies`を利用することにした．`dadarek/wait-for-dependencies`イメージは，`depends_on`で指定したコンテナの起動が確認できるまで他のコンテナの実行を待たせることができるので，PostgreSQLの起動が完了した後に，APPのコンテナを実行するように修正した．


```Dockerfile:docker-compose.yml

version: "2"
services:
  waitfordb:
      image: dadarek/wait-for-dependencies
      depends_on:
        - postgres
      command: postgres:5432
      
  postgres:
    image: postgres:10
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: postgre
      POSTGRES_PASSWORD: postgre
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=ja_JP.UTF-8"
    volumes:
      - ./db/psgl:/var/lib/postgresql/data
  
  app:
    build: .
    depends_on:
      - postgres
      - waitfordb
    environment:
        TZ: "Asia/Tokyo"
```
  
```sh:
$ docker-compose run --rm waitfordb
Creating dbdocker_postgres_1 ... done
Waiting for postgres to listen on 5432...
sleeping
sleeping
$ docker-compose up -d postgres app
dbdocker_postgres_1 is up-to-date
Starting dbdocker_waitfordb_1 ... done
Creating dbdocker_app_1       ... done
$ docker-compose logs
Attaching to dbdocker_app_1, dbdocker_postgres_1, dbdocker_waitfordb_1
postgres_1   | 2018-08-24 09:29:50.620 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
postgres_1   | 2018-08-24 09:29:50.620 UTC [1] LOG:  listening on IPv6 address "::", port 5432
postgres_1   | 2018-08-24 09:29:50.623 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
postgres_1   | 2018-08-24 09:29:50.714 UTC [24] LOG:  database system was shut down at 2018-08-24 09:28:38 UTC
postgres_1   | 2018-08-24 09:29:50.748 UTC [1] LOG:  database system is ready to accept connections
postgres_1   | 2018-08-24 09:29:51.849 UTC [31] LOG:  incomplete startup packet
postgres_1   | 2018-08-24 09:29:53.968 UTC [32] LOG:  incomplete startup packet
app_1        | /usr/local/lib/python3.6/site-packages/psycopg2/__init__.py:144: UserWarning: The psycopg2 wheel package will be renamed from release 2.8; in order to keep installing from binary please use "pi
p install psycopg2-binary" instead. For details see: <http://initd.org/psycopg/docs/install.html#binary-install-from-pypi>.
app_1        |   """)
app_1        | <connection object at 0x7f2953df5e88; dsn: 'user=postgre password=xxx dbname=postgres host=dbdocker_postgres_1 port=5432', closed: 0>
waitfordb_1  | Waiting for postgres to listen on 5432...
waitfordb_1  | sleeping
waitfordb_1  | sleeping
waitfordb_1  | Waiting for postgres to listen on 5432...
```

やっと繋がった！

# 終わりに

いろいろ探したところ起動順の問題を解決するには，自前でスクリプトを書くか`dadarek/wait-for-dependencies`イメージを利用する以外の解決策はなかった．

そもそもコンテナ起動と同時にデータベスに接続しようというのが，構築としてよくないのかもしれない．

##### ソースコード
最終的なファイル構成はこんな感じ
[GitHub - sey323/docker-postgresql-python: Docker上でPostgreSQLをPythonから扱うときの最小構成](https://github.com/sey323/docker-postgresql-python)