「postgresql on docker」ネタのうちのひとつ

この「のうちのひとつ」があなたの望むパターンである確率はきっと50%未満。

「on docker」で包まれたネタであるからには、「docker であること」をフルに活かしたネタのほうが「気分が良い」のである。つまりは、「postgresql だけを専門にサービスする docker コンテナを使う」、とりわけ、docker-compose で使うのが、考えることも少ないし、保守も簡単だし、いいことずくめだったりするわけだ。その向きには素直に公式イメージを使うだけである。だけであるといったって、redis だのほどには簡単ではないので、それについて詳しく書くことには価値はあるし、間違いなく需要はあると思うけれど、今回のはそれではなくて。

たとえば「AWS で仮想 OS を運用していて、それは docker には乗せていないが、テストをしやすくするために、模擬環境を docker コンテナとして構築したい」ということを考えるとする。このようなケースだと、「独立した postgresql コンテナを使う」よりは、「運用している AWS の仮想 OS ローカルに postgresql をインストールしているのならば、それも込みで模擬したい、つまり、docker コンテナ内に postgresql をインストールして動かしたい」と考えたい、ということ。出来るだけ実際の本番稼働と同じものを作りたいわけだ。つまりはこれ、「VirtualBox にもどき環境を構築する」のと本質的にはほとんど変わらないのだけれど、もちろん docker 固有の話も多少はある。

とにかく一つ実際に動くものが実例としてあるといろいろ便利だろう(今後の雛形になるかも)と思って、一つ作ってみたのがこれ(ら):

dockerbuild.sh
 1 #! /bin/sh
 2 entsh=dockerentry$$.sh
 3 trap 'rm -f ${entsh} requirements.txt Dockerfile' 0 1 2 3 15
 4 
 5 # -------------------
 6 _USER=mypg
 7 _DB=mydb
 8 _PGPASS=asdfg
 9 
10 # -------------------
11 cat << __END__ > ${entsh}
12 #! /bin/sh -x
13 (
14 cd `dirname $0`
15 # 2022-03-25追記: 後で気付いたが .pgpass を作るのは dockerentry.sh 内ではなく
16 #                 Dockerfile 内のビルドステップでやるべきだ。
17 echo 'localhost:5432:${_DB}:${_USER}:${_PGPASS}' > /home/${_USER}/.pgpass
18 chmod 600 /home/${_USER}/.pgpass
19 sudo -u postgres pg_ctlcluster 9.5 main start
20 sudo -u postgres createuser ${_USER} -d
21 sudo -u postgres createdb ${_DB} -O ${_USER}
22 
23 psql -d ${_DB} <<EOF
24 CREATE TABLE t_mal_people (
25   peopleid BIGINT PRIMARY KEY,
26   canonical VARCHAR,
27   english VARCHAR,
28   japanese VARCHAR
29 )
30 ;
31 CREATE TABLE t_mal_anime (
32   animeid BIGINT PRIMARY KEY,
33   canonical VARCHAR,
34   english VARCHAR,
35   japanese VARCHAR,
36   premiered VARCHAR,
37   type VARCHAR
38 );
39 \\copy t_mal_people FROM '/home/${_USER}/allpeople.csv' HEADER DELIMITER ',' CSV;
40 \\copy t_mal_anime FROM '/home/${_USER}/allanime.csv' HEADER DELIMITER ',' CSV;
41 EOF
42 )
43 exec "\$@"
44 __END__
45 
46 # -------------------
47 sed 's@ *#.*$@@' <<__END__ > requirements.txt
48 psycopg2-binary<2.9  # Dropped support for Python 2.7, 3.4, 3.5 on psycopg 2.9.
49 soupsieve<2.2  # Dropped support for Python 3.5 on soupsieve 2.2.
50 beautifulsoup4<4.11.0  # Dropped support for Python 3.5 on bs4 4.11.0.
51 certifi<2022.5.18  # Dropped support for Python 3.5 on certifi 2022.5.18.
52 requests<2.26.0  # Dropped support for Python 3.5 on requests 2.26.0.
53 markupsafe<2  # Dropped support for Python 3.5 on markupsafe 2.0.0.
54 jinja2<3  # Dropped support for Python 3.5 on jinja2 3.0.0.
55 flask<2  # Dropped support for Python 3.5 on flask 2.0.0.
56 __END__
57 
58 # -------------------
59 cat << __END__ > Dockerfile
60 FROM ubuntu:16.04
61 
62 ENV DEBIAN_FRONTEND=noninteractive
63 SHELL ["/bin/bash", "-c"]
64 RUN apt-get update -y && apt-get upgrade
65 
66 ENV LANG=C.UTF-8
67 ENV LC_ALL=C.UTF-8  # 2023-03-31より前に読んだ方、ごめんなさい、ミスでLCALLとしちゃってた
68 RUN apt-get install -yq --no-install-recommends postgresql postgresql-contrib postgresql-client libpq-dev
69 #RUN sed -i "/^#listen_addresses/alisten_addresses = '*'" /etc/postgresql/*/main/postgresql.conf
70 RUN apt-get install -yq --no-install-recommends sudo
71 RUN useradd -ms /bin/bash ${_USER}
72 RUN usermod -aG sudo ${_USER}
73 RUN echo '${_USER} ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers.d/${_USER} && chmod 0440 /etc/sudoers.d/${_USER}
74 
75 RUN apt-get install -yq --no-install-recommends python3 python3-pip
76 RUN apt-get install -yq --no-install-recommends python3-setuptools
77 COPY requirements.txt .
78 RUN python3 -m pip install -r requirements.txt
79 
80 USER ${_USER}
81 
82 COPY ${entsh} /home/${_USER}
83 RUN sudo chmod u+x /home/${_USER}/${entsh}
84 COPY *.csv /home/${_USER}
85 COPY app.py /home/${_USER}
86 ENTRYPOINT ["/home/${_USER}/${entsh}"]
87 CMD ["python3", "/home/${_USER}/app.py"]
88 __END__
89 
90 # -------------------
91 docker build -t ${1:-pg-xenial} .
92 # ex) docker run -it --rm -p=8080:5000 pg-xenial
app.py
 1 # -*- coding: utf-8 -*-
 2 import io
 3 import os
 4 import re
 5 import json
 6 
 7 import psycopg2
 8 import requests
 9 import bs4
10 
11 
12 _MALROOT = "https://myanimelist.net"
13 
14 
15 def _get_roles_of_person(peopleid, types_=["character"]):
16     def _froma(a):
17         resurl = a.attrs["href"]
18         t, id = resurl[len(_MALROOT) + 1:].split("/")[:2]
19         return t, int(id)
20 
21     url = "{}/people/{}".format(_MALROOT, peopleid)
22     cont = requests.get(url).text
23     soup = bs4.BeautifulSoup(cont, "html.parser")
24     for e in soup.find_all(class_=re.compile("(ga-click|picSurround)")):
25         e.decompose()
26     for class_ in ("js-table-people-character", "js-table-people-staff"):
27         typ = class_[len("js-table-people-"):]
28         if typ not in types_:
29             continue
30         tbl = soup.find("table", class_=class_)
31         if tbl is None:
32             continue
33         for tr in tbl.find_all("tr"):
34             yield typ, [_froma(a) for a in tr.find_all("a")]
35 
36 
37 def _get_common_animes(peopleids):
38     common = set()
39     for peopleid in peopleids:
40         wkanimes = set([role[0][-1] for _, role in _get_roles_of_person(peopleid)])
41         if len(common) == 0:
42             common |= wkanimes
43         else:
44             common &= wkanimes
45     return list(sorted(list(common)))
46 
47 
48 #print(_get_common_animes([6997, 6996]))  #19834
49 conn = psycopg2.connect("dbname=mydb user=mypg")
50 conn.set_client_encoding("utf-8")
51 from flask import Flask, request
52 app = Flask(__name__)
53 
54 
55 @app.route("/common_animes/")
56 def common_animes():
57     req = request.get_json()
58     peoplelist_in = req["peoplelist"]
59     peoplelist = []
60     with conn.cursor() as cur:
61         for i, p in enumerate(peoplelist_in):
62             if isinstance(p, (int,)):
63                 cur.execute(
64                     """SELECT peopleid, japanese FROM t_mal_people
65                     WHERE peopleid = %s""",
66                     (p,))
67             else:
68                 cur.execute(
69                     """SELECT peopleid, japanese FROM t_mal_people
70                     WHERE japanese = %s OR english = %s""",
71                     (p, p,))
72             peoplelist.append(cur.fetchone())
73     print(peoplelist)
74     common = _get_common_animes([p[0] for p in peoplelist])
75     with conn.cursor() as cur:
76         cur.execute(
77             "SELECT animeid, japanese FROM t_mal_anime WHERE animeid IN %s",
78             (tuple(common),))
79         rescommon = [r for r in cur]
80 
81     return json.dumps({"people": peoplelist, "common": rescommon})
82 
83 
84 if __name__ == '__main__':
85     app.run(host="0.0.0.0")
allanime.csv (抜粋)
1 AnimeId,Canonical,English,Japanese,Premiered,Type
2 1,Cowboy_Bebop,Cowboy Bebop,カウボーイビバップ,Spring 1998,TV
3 5,Cowboy_Bebop__Tengoku_no_Tobira,Cowboy Bebop: Tengoku no Tobira,カウボーイビバップ 天国の扉,,Movie
4 6,Trigun,Trigun,トライガン,Spring 1998,TV
5 7,Witch_Hunter_Robin,Witch Hunter Robin,Witch Hunter ROBIN (ウイッチハンターロビン),Summer 2002,TV
6 8,Bouken_Ou_Beet,Bouken Ou Beet,冒険王ビィト,Fall 2004,TV
7 15,Eyeshield_21,Eyeshield 21,アイシールド21,Spring 2005,TV
allpeople.csv (抜粋)
 1 PeopleId,Canonical,English,Japanese
 2 1,Tomokazu_Seki,"Seki, Tomokazu",関 智一
 3 2,Tomokazu_Sugita,"Sugita, Tomokazu",杉田 智和
 4 3,Satsuki_Yukino,"Yukino, Satsuki",ゆきの さつき
 5 4,Aya_Hirano,"Hirano, Aya",平野 綾
 6 5,Kenichi_Suzumura,"Suzumura, Kenichi",鈴村 健一
 7 6,Toshiyuki_Morikawa,"Morikawa, Toshiyuki",森川 智之
 8 7,Eiji_Yanagisawa,"Yanagisawa, Eiji",柳沢 栄治
 9 8,Rie_Kugimiya,"Kugimiya, Rie",釘宮 理恵
10 9,Kotono_Mitsuishi,"Mitsuishi, Kotono",三石 琴乃
11 10,Johnny_Yong_Bosch,"Bosch, Johnny Yong",
12 11,Kouichi_Yamadera,"Yamadera, Kouichi",山寺 宏一
13 12,Steven_Blum,"Blum, Steven",
14 13,Ayako_Kawasumi,"Kawasumi, Ayako",川澄 綾子
15 14,Megumi_Hayashibara,"Hayashibara, Megumi",林原 めぐみ
16 15,Junko_Takeuchi,"Takeuchi, Junko",竹内 順子
17 16,Noriaki_Sugiyama,"Sugiyama, Noriaki",杉山 紀彰
18 17,Akira_Ishida,"Ishida, Akira",石田 彰

flask で REST API サービスを作りこんでいるので、その部分を除いた部分が「postgresql on docker」としての本質部分ということになるのだが、はっきりいってしまえば「postgresql on ubuntu」ネタそのものなので、逐一の説明はしない。docker のライフサイクルについて慣れていないと dockerentry.sh の意味がわからないと思うんだけれど…、これはやってるうちにわかるかなと思う。なんとなく「なんで run のたびにまっさらな初期化をするの? build 時に出来ないの?」と思ってしまいそうになるんだけれど、それは出来たとしてもあまり嬉しくない(ボリュームマッピングの活用が出来ないなどの理由で)。

「docker 固有の話」は、「System has not been booted with systemd as init system (PID 1). Can’t operate.」関係なんだけれど、dockerentry.sh には現れていないのでまぁいいよね、これはたとえば「service postgresql start」などは使えない、というだけの話。

なお、「今日のワタシのニーズ向け」に書いたものなので、「ubuntu:16.04」という凄まじく古いものを作っている。完全にワタシのニーズと一致してる人はいいが、ほとんど全ての人にとってはこれは当てはまらないはずなので、適宜読み替えること。むろん「ubuntu」もあなたの望むものと違うこともあろう。debian/ubuntu 系でないなら Dockerfile の記述はかなり異なったものになる。

dockerbuild.sh して docker run (たとえば「docker run -it --rm -p=8080:5000 pg-xenial」)すると以下が動かせる:

cli.py
 1 # -*- coding: utf-8 -*-
 2 from pprint import pprint
 3 import requests
 4 
 5 
 6 # allanime.csv, allpeople.csv が完全であれば以下が動く(が完全でないなら動かない)
 7 res = requests.get(
 8         "http://localhost:8080/common_animes/",
 9         json={"peoplelist": ["Kurosawa, Tomoyo", "Hanamori, Yumiri"]}).json()
10 pprint(res)

結果は例えば:

 1 {'common': [[33737, 'メガトン級ムサシ'],
 2             [35883, 'シンデレラガールズ劇場 第2期'],
 3             [38474, 'ゆるキャン△ SEASON2'],
 4             [38475, '映画 ゆるキャン△'],
 5             [39355, 'ラディアン 第2シリーズ'],
 6             [41433, 'アクダマドライブ'],
 7             [49909, 'コタローは1人暮らし'],
 8             [50559, 'メガトン級ムサシ'],
 9             [52093, 'TRIGUN STAMPEDE']],
10  'people': [[11661, '黒沢 ともよ'], [21543, '花守 ゆみり']]}

「一つ実例があると便利」と言ったけれど、ほんとはもう一歩あるとさらに良いのよね。pg_dump したバックアップデータを初期データとすることが出来る、というやつね。あと、公式イメージがやってるように PGDATA のコントロールも出来たほうが便利とも思うし。まぁそれらはそのうち…。



Related Posts