技術メモなど

ほぼ自分用の技術メモです。

Squidで特定ドメインまたはURLの場合のみ上位プロキシを通し、それ以外は上位プロキシを通さない振り分け設定

Squidで特定のドメインの場合は指定した上位プロキシ経由でアクセスし、それ以外は上位プロキシを通さず自身でアクセスするようにする設定です。また、アクセス方法がHTTPSでなく平文のHTTPである場合のみ、特定のURLで振り分けることも可能です。

クラウドの業務システム向け通信は上位プロキシ、それ以外のインターネット向け通信は全て自プロキシ、といったことが実現できます。

検証はSquidのバージョン3.5で行っています。

なお、多段プロキシ構成で特定ドメインまたはURLで上位プロキシを振り分ける設定の場合は以下のページにまとめています。

takeda-h.hatenablog.com

概要

/etc/squid/squid.confに次の通り記載。

  • cache_peerに上位プロキシを定義。
  • aclに振り分けたい特定ドメインを定義。
  • cache_peer_accessに振り分けたい特定ドメインを定義したaclと経由したい上位プロキシを定義したcache_peerを紐付け。
  • never_direct allow acl_name, always_direct deny acl_name, always_direct allow allを記載すると、aclに当てはまらない通信は全て上位プロキシを経由せず自身でアクセスするようになる。

設定例

構成

多段プロキシ構成は以下の通りとします。上位プロキシには振り分けに関する設定は不要で、全て下位プロキシ側に設定することになります。

プロキシサーバ ホストと待受けポート
Aプロキシ(特定ドメインまたはURLの場合経由させたい上位プロキシ) system-proxy.example.com:3128
Bプロキシ(クライアント側に設定しているプロキシ〔下位プロキシ〕) client-proxy.example.com:8080

下位プロキシ側(Bプロキシ)の設定

コンフィグ例

/etc/squid/squid.confに以下の通り追記(または変更)します。

# 上位プロキシの定義
# Aプロキシ(特定ドメインまたはURLの場合経由させたい上位プロキシ)
cache_peer system-proxy.example.com parent 3128 0 no-query

# 振り分けたい特定ドメイン・URLの定義
# 振分け対象ドメイン
acl system-domain dstdomain "/etc/squid/system-domain.acl"
# 振り分け対象URL(ただしHTTPSだとURLで振り分けることはできないのでドメインで振り分けるしかない)
acl system-url url_regex "/etc/squid/system-url.acl"

# 振分け対象特定ドメイン・URLとAプロキシの紐付け
cache_peer_access system-proxy.example.com allow system-domain
cache_peer_access system-proxy.example.com allow system-url

# 振分け対象特定ドメイン・URL以外は全てBプロキシ(自分自身)経由にする
never_direct allow system-domain
never_direct allow system-url
always_direct deny system-domain
always_direct deny system-url
always_direct allow all

ポイントは、

  • never_direct allowとalways_direct denyの両方にACLをセットで設定する必要があること。
  • 最後にalways_direct allow allをすること。

です。

振分け対象ドメインの指定

振り分けたい特定ドメインをここではsystem-domainという名前(名前は任意で大丈夫)のaclに定義しています。

acl system-domain dstdomain "/etc/squid/system-domain.acl"

dstdomainの後に対象ドメインをベタ書きしてもいいのですが、メンテしやすいよう/etc/squid/system-domain.aclというテキストファイルを作成し、対象ドメインを列記するようにします。

/etc/squid/system-domain.aclの内容は次の通りです。振り分けたい特定ドメインがsystem01.example.comというドメインと、system02.example.comサブドメインという想定です。

system01.example.com
.system02.example.com

振分け対象URLの指定

同様に、振り分けたい特定URLをsystem-urlという名前のaclに定義しています。/etc/squid/system-url.aclというテキストファイルを作成し、対象URLを列記するようにします。

acl system-url url_regex "/etc/squid/system-url.acl"

ポイントは、aclのurl_regex対象URLを正規表現で記載できることです。dstdomainの方は正規表現は使えないので注意です。

ただし、HTTPSの場合はURLでの振分けは使えません。後で検証の際にSquidaccess.logを見れば分かるのですが、HTTPSの場合、クライアントと通信先サーバとの間で暗号化されているので参照先URLがSquidからは見えないためだと思われます。

/etc/squid/system-url.aclの内容は次の通りです。振り分けたい特定URLがhttp://system03.example.com/maintener/以下という想定です。

^http://system03\.example\.com/maintener/.*$

設定の反映

コンフィグに誤りがないことが確認できたら、Bプロキシのsquidサービスをリロード(または再起動)します。

# systemctl reload squid

振分けができているか確認

期待した通りに振分け設定ができていれば、Bプロキシのaccess.logには次のようなログが出力されます。

651285320.368 1663 198.51.100.123 TCP_TUNNEL/200 3878 CONNECT system01.example.com:443 - FIRSTUP_PARENT/192.0.2.1 -

上位プロキシ側は次のようなログが出力されます。

651285320.370  1441 192.0.2.1 TCP_TUNNEL/200 3139 CONNECT system01.example.com:443 - HIER_DIRECT/203.0.113.200 -

Squidの多段プロキシ構成で特定ドメインまたはURLで上位プロキシを振り分ける設定

Squidで多段プロキシ構成で複数の上位プロキシが存在している時に、特定のドメインの場合は指定した上位プロキシ経由でアクセスするようにする設定です。また、アクセス方法がHTTPSでなく平文のHTTPである場合のみ、特定のURLで振り分けることも可能です。

クラウドの業務システム向け通信はAプロキシ、それ以外のインターネット向け通信は全てBプロキシ、といったことが実現できます。

検証はSquidのバージョン3.5で行っています。

概要

/etc/squid/squid.confに次の通り記載。

  • cache_peerに上位プロキシを定義。この時デフォルトのプロキシは必ず他の上位プロキシを定義する行より下に記載しないとならない。
  • aclに振り分けたい特定ドメインを定義。
  • cache_peer_accessに振り分けたい特定ドメインを定義したaclと経由したい上位プロキシを定義したcache_peerを紐付け。
  • never_direct allow allを記載すると、aclに当てはまらない通信は全てデフォルトの上位プロキシを経由するようになる。

設定例

構成

多段プロキシ構成は以下の通りとします。上位プロキシには振り分けに関する設定は不要で、全て下位プロキシ側に設定することになります。

プロキシサーバ ホストと待受けポート
Aプロキシ(特定ドメインまたはURLの場合経由させたい上位プロキシ) system-proxy.example.com:3128
Bプロキシ(それ以外の場合経由させたいデフォルトの上位プロキシ) internet-proxy.example.com:3128
Cプロキシ(クライアント側に設定しているプロキシ〔下位プロキシ〕) client-proxy.example.com:8080

下位プロキシ側(Cプロキシ)の設定

コンフィグ例

/etc/squid/squid.confに以下の通り追記(または変更)します。

# 上位プロキシの定義
# Aプロキシ(特定ドメインまたはURLの場合経由させたい上位プロキシ)
cache_peer system-proxy.example.com parent 3128 0 no-query
# Bプロキシ(それ以外の場合経由させたいデフォルトの上位プロキシ)
# デフォルトのプロキシは必ず他の上位プロキシより下の行に書くこと!
cache_peer internet-proxy.example.com parent 3128 0 default no-query

# 振り分けたい特定ドメイン・URLの定義
# 振分け対象ドメイン
acl system-domain dstdomain "/etc/squid/system-domain.acl"
# 振り分け対象URL(ただしHTTPSだとURLで振り分けることはできないのでドメインで振り分けるしかない)
acl system-url url_regex "/etc/squid/system-url.acl"

# 振分け対象特定ドメイン・URLとAプロキシの紐付け
cache_peer_access system-proxy.example.com allow system-domain
cache_peer_access system-proxy.example.com allow system-url

# 振分け対象特定ドメイン・URL以外は全てBプロキシ経由にする
never_direct allow all

解説

上位プロキシの設定

ここで重要なのが、デフォルトの上位プロキシを指定する記載は必ず他の上位プロキシを指定する行より後にする必要があることです。defaultというキーワードを指定していてもなぜか関係ありません。他の上位プロキシの記載が後にあるとそちらがデフォルトになってしまい、意図した通りになりません。

cache_peer system-proxy.example.com parent 3128 0 no-query
cache_peer internet-proxy.example.com parent 3128 0 default no-query # こちらがデフォルトの上位プロキシ

Squidのドキュメントを読んでも、複数のdefaultを指定した上位プロキシがある場合は最初のものが使われる、との記述があるも、実際にコンフィグに投入しても意図した通りにはなりませんでした。また、cache_peer自体の順序についても記述はありません。ですが実際には一番最後に指定したcache_peerがデフォルトになってしまうので、cache_peerの記述順序は要注意です。

==== PEER SELECTION METHODS ====

The default peer selection method is ICP, with the first responding peer being used as source. These options can be used for better load balancing.

default This is a parent cache which can be used as a "last-resort" if a peer cannot be located by any of the peer-selection methods. If specified more than once, only the first is used.

http://www.squid-cache.org/Versions/v3/3.5/cfgman/cache_peer.html

振分け対象ドメインの指定

振り分けたい特定ドメインをここではsystem-domainという名前(名前は任意で大丈夫)のaclに定義しています。

acl system-domain dstdomain "/etc/squid/system-domain.acl"

dstdomainの後に対象ドメインをベタ書きしてもいいのですが、メンテしやすいよう/etc/squid/system-domain.aclというテキストファイルを作成し、対象ドメインを列記するようにします。

/etc/squid/system-domain.aclの内容は次の通りです。振り分けたい特定ドメインがsystem01.example.comというドメインと、system02.example.comサブドメインという想定です。

system01.example.com
.system02.example.com

振分け対象URLの指定

同様に、振り分けたい特定URLをsystem-urlという名前のaclに定義しています。/etc/squid/system-url.aclというテキストファイルを作成し、対象URLを列記するようにします。

acl system-url url_regex "/etc/squid/system-url.acl"

ポイントは、aclのurl_regex対象URLを正規表現で記載できることです。dstdomainの方は正規表現は使えないので注意です。

ただし、HTTPSの場合はURLでの振分けは使えません。後で検証の際にSquidaccess.logを見れば分かるのですが、HTTPSの場合、クライアントと通信先サーバとの間で暗号化されているので参照先URLがSquidからは見えないためだと思われます。(SquidHTTPSを終端するようなman in the middle的設定ができるなら可能なのかも。)

そのため、HTTPSの場合はURLでの振分けは諦めて、ドメインで振り分けるしかなさそうです。

/etc/squid/system-url.aclの内容は次の通りです。振り分けたい特定URLがhttp://system03.example.com/maintener/以下という想定です。

^http://system03\.example\.com/maintener/.*$

ちなみにHTTPS(標準ポート)でドメインの振分けで正規表現が使いたい場合は、dstdomainでは正規表現が使えないためurl_regexを使って次のような書き方ができると思います。

# HTTPSで正規表現を使ったドメイン振り分け例(dstdomainではできないことに注意)
^branch[0-9]+\.system03\.example\.com:443$
# HTTPもある場合を想定した書き方
^(http://)*branch[0-9]+\.system03\.example\.com(:443)*(/.*)*$

指定以外の通信は全てデフォルトの上位プロキシに向ける設定

最後に以下の記述がないと下位プロキシが上位プロキシを経由せず直接通信してしまいます。

never_direct allow all

設定の反映

コンフィグに誤りがないことが確認できたら、Cプロキシのsquidサービスをリロード(または再起動)します。

# systemctl reload squid

振分けができているか確認

期待した通りに振分け設定ができていれば、Cプロキシのaccess.logには次のようなログが出力されます。

651285320.368 1663 198.51.100.123 TCP_TUNNEL/200 3878 CONNECT system01.example.com:443 - FIRSTUP_PARENT/192.0.2.1 -

上位プロキシ側は次のようなログが出力されます。

651285320.370  1441 192.0.2.1 TCP_TUNNEL/200 3139 CONNECT system01.example.com:443 - HIER_DIRECT/203.0.113.200 -

既存のHerokuで動かしているFlask + GunicornのアプリにNGINXのリバースプロキシを追加する方法 (Heroku Buildpack)

概要

  • Heroku Buildpack: NGINX を使えばHerokuでNGINXを導入できるのでHerokuにデプロイしているアプリケーションにリバースプロキシを設置できる。
  • アプリケーションのWebサーバがGunicornの場合、GunicornをUNIXソケットで待ち受けるように設定変更が必要。
  • 基本的に Heroku Buildpack: NGINX の手順通りやれば動いた。サンプルにはWebサーバにRubyUnicornを使う手順が記載されているが、適宜PythonのGunicornに置き換えれば大丈夫。アプリケーションはFlaskで動かしているが、DjangoでもGunicornを使うなら動くのではないかと思う。

きっかけ

旭川市新型コロナウイルスまとめサイトGooglePage Speed Insights で計測したらモバイルページの点数が低かったので、リバースプロキシ入れてキャッシュ時間をコントロールすれば改善できると思ったから。しかしオチを先に書くと、自分の書いたnginx.confがしょぼいのか導入前後でスコアは変わらなかった。(たぶんHerokuってログに記録される送信元IPアドレスがローカルIPアドレスなところを見るとデフォルトでリバースプロキシ越しに最適化されてアクセスされるように設計されている?)

ちなみに最初DockerでGunicorn動かすコンテナとNGINX動かすコンテナ2つ動かせばいけるのかなと思ったのだけど、自分が調べた範囲ではHerokuだと無理っぽい。なので開発環境ではDocker ComposeでGunicorn + Flaskが動くApplicationコンテナとNGINXが動くWeb Serverコンテナを使い、Heroku上の本番環境ではHeroku BuildpackのNGINXを使うようにしている。

手順

GunicornをUNIXソケットで待ち受けるよう変更

アプリのルートディレクトリに次のような gunicorn.conf.py を配置し、Gunicorn起動時に -c オプションで gunicorn.conf.py を指定すれば、通常のTCPソケットで待ち受けるモードからUNIXソケットで待ち受けるモードに変更できる。

def when_ready(server):
    open("/tmp/app-initialized", "w").close()


bind = "unix:///tmp/nginx.socket"

UNIXソケット通信で使うファイル名はHeroku Buildpacks: NGINXの仕様に合わせておく。

Requirements (Proxy Mode)

Your webserver listens to the socket at /tmp/nginx.socket.

You touch /tmp/app-initialized when you are ready for traffic.

You can start your web server with a shell command.

heroku/heroku-buildpack-nginx - Buildpacks - Heroku Elements

リバースプロキシで動かすためのNGINXの設定ファイルを作成

アプリのルートディレクトリに config ディレクトリを作成し、その中に nginx.conf.erb を作成する。Heroku Buildpack: NGINXの Githubリポジトリnginx.conf.erb のサンプル があるのでこれをベースにお好みで設定する。

daemon off;
worker_processes <%= ENV['NGINX_WORKERS'] || 4 %>;

events {
    use epoll;
    accept_mutex on;
    worker_connections <%= ENV['NGINX_WORKER_CONNECTIONS'] || 1024 %>;
}

http {
    gzip on;
    gzip_comp_level 2;
    gzip_min_length 512;
    gzip_proxied any;

    server_tokens off;
    log_format l2met 'measure#nginx.service=$request_time request_id=$http_x_request_id';
    access_log <%= ENV['NGINX_ACCESS_LOG_PATH'] || 'logs/nginx/access.log' %> l2met;
    error_log <%= ENV['NGINX_ERROR_LOG_PATH'] || 'logs/nginx/error.log' %>;

    include mime.types;
    default_type application/octet-stream;
    sendfile on;

    tcp_nopush on;
    keepalive_timeout 65;

    upstream app_server {
        server unix:///tmp/nginx.socket fail_timeout=0;
    }

    server {
        listen <%= ENV["PORT"] %>;

        location / {
            proxy_redirect off;
            proxy_pass http://app_server;
        }

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

ちなみに nginx.conf.erb の設置パスに注意。Heroku Buildpacks: NGINXのページには次の通りSolo Modeの要求事項としてしか記載されていないが、Proxy Modeでも config/nginx.conf.erb に設置しないと読み込んでくれない。

Requirements (Solo Mode)

Add a custom nginx config to your app source code at config/nginx.conf.erb.

heroku/heroku-buildpack-nginx - Buildpacks - Heroku Elements

Heroku Buildpacks: NGINXのインストール

Heroku Toolbeltはインストール済みとする。ターミナルから次のコマンドでHeroku Buildpacks: NGINXを追加する。

heroku buildpacks:add heroku-community/nginx

Procfile の修正

Heroku Buildpacks: NGINXを使うには、アプリのルートディレクトリにある Procfile を修正する。

まず、動かしたいFlaskアプリがルートディレクトリにある次のような hello.py だとする。

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'


if __name__ == "__main__":
    app.run()

これを普通にHerokuでWebサーバをGunicornで動かすには、以下のような Procfile になっていると思う。

gunicorn hello:app --log-file -

ここから、Procfile を次の通り変更する。

web: bin/start-nginx gunicorn hello:app -c gunicorn.conf.py --log-file=-

あとはHerokuの本番リポジトリに変更をPushすれば、NGINXのリバースプロキシ経由でGunicornにアクセスされるようになる。

感想

現状 Page Speed Insights はNGINX追加前後でスコアは変わらなかった。 旭川市新型コロナウイルスまとめサイト はUDフォントを使いたくてWebフォントを使っているし、FontAwsomeも使っているため、これでだいぶスコアが落ちてしまっているから仕方ないかもしれない。キャッシュを効かせるとしたら大きめのメディアファイルにだと思うのだけど、容量が大きいのがグラフの画像ファイルで、これは一応準リアルタイムで配信したいので、キャッシュ時間を10分に設定してみたがあまり効果なさそう。

参考

旭川市の新型コロナワクチン接種医療機関の地図や予約受付状況を確認できるページを作りました

概要

シビックテックとして、旭川市公式ホームページに掲載されている新型コロナウイルス感染症の情報のまとめ的なサイトをやっているのですが、新たに新型コロナワクチン接種医療機関の地図や予約受付状況を確認できるページを作りました。個人による二次情報なので正確性は保証できないため、あくまで参考にしかならないものですが、引っ越してきたばかりの人とか旭川の地理に明るくない人がワクチン接種医療機関を探すのに少しでも役に立てばいいなと思っています。

3回目接種 : コロナワクチンマップ(3回目接種) | 旭川市新型コロナウイルスまとめサイト

5~11歳接種 : コロナワクチンマップ(5~11歳接種) | 旭川市新型コロナウイルスまとめサイト

1・2回目接種 : コロナワクチンマップ(1・2回目接種) | 旭川市新型コロナウイルスまとめサイト

このページの使い方

地区から探す

f:id:takeda_h:20210920182253p:plain

対象が16歳以上の医療機関と、12歳から15歳までの医療機関に分けて地区名のリストにしてますので、探したい地区のリンクにアクセスすると、その地区の医療機関の一覧と地図、取得できていれば予約受付状況を表示します。

f:id:takeda_h:20210920225216p:plain

f:id:takeda_h:20210920182945p:plain

医療機関名か、地図のポップアップのリンクにアクセスすると、医療機関の個別情報を表示します。

f:id:takeda_h:20210920183805p:plain

場所から探す

地図上のマーカーをクリックすると該当の医療機関の個別情報へのリンクがポップアップします。

f:id:takeda_h:20210920210323p:plain

このページの仕組み

ワクチン接種医療機関一覧の取得

旭川市公式ホームページの 新型コロナワクチン関連情報 | 旭川市 から 予約受付情報・予約システム接種会場 と進むと、旭川市内の新型コロナワクチン接種医療機関の一覧が掲載されています。一覧は16歳以上が対象の医療機関と、12歳から15歳までが対象の医療機関に分かれて、更に地区ごとに分かれて1ページに掲載されているので、これをPythonの定番ライブラリである BeautifulSoup を使ってデータ抽出しています。

予約受付状況の取得

現在予約受付状況はHTMLでの掲載はなく、予約受付状況・予約システム | 旭川市予約受付状況がPDFで掲載 されています。PDFファイルのスクレイピングが一般的に非常に困難なのですが、幸いなことに内容が表形式になっており、tabula-py という素晴らしいライブラリを使うことでpandas DataFrameでデータ抽出することができました。

github.com

ただ、PDFの元データがHTMLのtable要素のように構造化されているわけではないので、定期的に確認しないと内容の表の形が変わって列ずれなど意図しない形でデータ抽出してしまうことになりそうです。

位置情報の取得

旭川市公式ホームページに医療機関の位置情報までは掲載されていないので、Yahoo!ローカルサーチAPI を使って医療機関名から緯度経度を取得しています。なるべく正しい検索結果を得るため、APIのパラメータで市区町村と業種を指定してリクエストしています。それでも一部の医療機関の検索結果が0件だったため、これらの医療機関だけ手動で緯度経度を設定しました。ちなみにページのアクセスの都度取得すると無駄が多いので、クローラで旭川市公式ホームページの情報を取得するとき(後述)にまとめて緯度経度を取得し、データベースへ保存するようにしています。

医療機関と予約受付状況、位置情報の紐付けについて

旭川市公式ホームページの情報に医療機関を一意に特定するキーがあるわけじゃないため、名称のみで紐付けしています。そのため、一部予約受付状況が表示できていない医療機関があります。

地図について

leaflet というJavaScriptのライブラリを使って OpenStreetMap を表示しています。マーカーは Font Awesome で病院のアイコンを表示するようにしています。

更新スケジュール(クローリング)について

Heroku Scheduler で一定時間ごとにPython製のクローラを実行して情報を更新しています。個人が趣味で作っているものなので、元データの構造が変わって処理がコケてたら、仕事から帰ってきてから原因調べて修正して……みたいなベストエフォート運用になっています。

そのため、各情報のページの先頭には、正確な情報は 旭川市公式ホームページ を参照して欲しい旨の注意書きと、一応データの更新日時を表示するようにしています。

このページを作ったきっかけ

Code for Japan Summit 2021 Online with 東北 を視聴して、何となく自分も手を動かしたくなったため。

summit2021.code4japan.org

おわりに

どこまで続けられるか分かりませんが、自分の技術力が追いつく限りは運用していこうと思います。

ちなみにこのサイトでは他に旭川市新型コロナウイルス感染症の新規感染者数などを集計して公開しています。

f:id:takeda_h:20210920223730p:plain
こんな感じでグラフを表示したりしています。

ash-unofficial-covid19.herokuapp.com

このサイトで公開しているデータをデータソースに使ってくれているサイトがありました。

リアルタイムな地域への感染症関連情報の提供サイト Real-time Local Information Provider

こちらのサイトでは北海道ほか色んな地域の新型コロナウイルス感染状況をビジュアル化していて、そのデータソースの一つに 感染者の状況非公式オープンデータ を使ってくれていました。運用続けていて良かったと思いました。

ゆるい勉強会@旭川に参加した

ゆるくはじめよう@あさひかわ - connpass に参加したので感想などを書きます。

asahikawa.connpass.com

  • 緊急事態宣言中のためzoomでオンライン開催。自宅に自分の部屋がなく生活音というか騒音丸聞こえになるのでやむなくマイクもカメラもオフで参加。それでもzoom内のchatやtwitterハッシュタグ 付けてつぶやいた内容拾ってもらえて何とか輪に入れてよかった。
  • 13:00に本編が始まったタイミングと昼食のタイミングが重なってしまい、1人目の発表を半分以上聞き逃してしまった。twitterのログ追っかけたら出欠自動化とか危険な香り(技術的にはいい意味で)のする面白キーワード出てきて聞き逃しをすごく後悔。
  • 発表全部面白かった。非IT (ICT?) 系の職なので、今回発表で挙がってた技術に触れてるような人が周囲に皆無なため、普段ネットでしか目にできない技術に触れてる人が地元にもいるんだということがうれしい。
  • ただ、皆さんのお話伺うかぎり、情報系の会社でも情報工学系の学域でも、周りに同じ技術分野に興味がある人が少なくて苦労されているような。Web系でフロントエンドに興味があればdocker使えれば環境簡単に整備できるし、dockerじゃなくても開発環境のVMとりあえずコピーしてこればとりあえず動くみたいなのも、開発環境のセットアップでまず躓いて嫌になってしまう人を減らせるし、これはとても望ましい方向だと思うけど、裏で動いているコンテナの仕組みだとか、Linuxで環境一からセットアップする技術とかに興味を持つ人も一定数いた方が絶対面白いと思います。自分はサーバOSセットアップからネットワークのL7からL2まで、物理層はラックマウントからLANの配線とかまで好きな人間です。
  • (ちなみに弊社のITリテラシは、Excelをちゃんと表として使うとかデータ構造を正規化して再利用しやすくするとかのレベルで使えてる人が圧倒的少数で、VBAで自動化なんてもってのほか、なんなら全部紙に印刷して目視で確認が正義みたいな価値観が蔓延してる感じでとても辛いです)
  • (なのにRPAには興味があるみたいで意味不明です。あれ相当ちゃんと考えて作らないと野良VBAよりよっぽどたちの悪いもの錬成されませんかね?)
  • 公共系とのお仕事の辛み、よく分かります……。公共系、予算があって予算とは当然議会の承認(住民の承認)があってのものなので、予算にない事業はできない=機動力に欠ける(当初予算は前年度から要求するから)のは如何ともしがたいところかと思います。
  • 次は皆さんの開発環境(好きなエディタ)とかの話聞きたいなー。

最後に、どさくさにまぎれて趣味で作ったWebアプリ ( 旭川市内の最新感染動向 | 旭川市新型コロナウイルスまとめサイト ) を宣伝させてもらいました。つたないアプリだけど反応をもらえて本当にうれしかったです。モチベーション爆上げしました。 一応ソースは GitHub - takedah/ash_unofficial_covid19 にあります。

ash-unofficial-covid19.herokuapp.com

ちなみにこのアプリ、累積患者数が今年の5月中旬くらいから1人アプリの集計と市の発表とずれてしまってます。一旦DB空にしてスクレイピングし直しても変わらないので原因不明。

Python + Flask + Heroku + Heroku Postgres で無料の範囲内の Web アプリを公開する手順メモ

Heroku で Flask のアプリを動かしたりPostgreSQL を無料の範囲内で使う手順のメモです。(無料版の制限事項は Heroku を使って無料でアプリを開発 参照。)

事前に Heroku のアカウントを作成しておく必要があります。なお、無料の範囲内でも PostgreSQL を使いたい場合はクレジットカードの登録が必要です。 Heroku のアカウントを作成後、 Heroku Dashboard から Account Settings - Billing に遷移すればクレジットカードを登録できます。

Heoroku CLI のインストール

Heroku を使う時、ほとんどの操作がダッシュボードから行えるとは思いますが、コマンドで操作できるようにしておいた方が都合が良い場合もあるので Heroku CLI をインストールしておきます。 Heoroku Dev Center で OS 別のインストール方法が記載されているので、環境に合った方法でインストールできます。自分は tarball を /usr/local/lib/heroku に展開し、 /usr/local/bin/heroku にシンボリックリンクを貼るようにしています。

ローカルでの下準備 (Git)

Heroku でアプリを公開するには、アプリを Git で管理することが必要です。アプリの作成と更新は、 Heroku のリモートリポジトリに push することで行うことになります。例として、次のような Python の Flask で作成したアプリを Heroku で公開することにします。

$ mkdir yourprojectname
$ cd ./yourprojectname
$ touch hello.py
$ touch requirements.txt

hello.py の中身は Quickstart — Flask Documentation (1.x.x) のまんまです。

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'


if __name__ == "__main__":
    app.run()

requirements.txt にはインストールしたいライブラリを記載します。

flask

ローカルでアプリが動くかテストします。 requirements.txt から Flask をインストールし、起動してみます。起動する前に環境変数の設定も必要です。

$ pip install -r requirements.txt
$ export FLASK_APP=hello.py
$ flask run

ブラウザで http://localhost:5000/ にアクセスし、 Hello World! とブラウザに表示されれば成功です。

テストに成功したら、このアプリを Git 管理下に置きます。

$ git init
$ git add .
$ git commit -m "first commit."

Heroku の設定

Heroku にログイン

ここまでできたら Heroku CLI でアプリを公開する下準備をします。まずは Heroku にログインします。

$ heroku login
heroku: Press any key to open up the browser to login or q to exit:
Opening Browser to https://cli-auth.heroku.com/auth/cli/browser/hogefuga

適当なキーを押すとブラウザが起動して Heroku のログイン画面が表示されるのでログインします。ログインに成功するとターミナルにメッセージが表示され、以降 Heroku の操作が可能になります。

Logging in... done
Logged in as hoge@example.com

Heroku でアプリの作成

Heroku でアプリを作成するのはブラウザから GUI でできますが、 Heroku CLI で Heroku にアプリを作成すると Git リモートリポジトリの設定を自動で行ってくれるのでコマンドから行います。

$ heroku create yourprojectname
$ git remote -v
heroku  https://git.heroku.com/yourprojectname.git (fetch)
heroku  https://git.heroku.com/yourprojectname.git (push)

作成したアプリが無料版になっているか確認します。 Heroku Dashboard から作成したアプリの管理画面を開き、Overview にて Dyno Formation が This app is using free dynos となっていれば無料版です。

Heroku で環境変数の設定

Quickstart — Flask Documentation (1.x.x) に沿って Flask を動かすためには環境変数の設定が必要なので、 Heroku CLI で行います。

$ heroku config:set FLASK_APP=hello.py
$ heroku config:get FLASK_APP
hello.py

アプリの公開

ここまで準備ができたら、 Heroku のリモートリポジトリに push するだけでアプリが公開できます。

$ git push heroku main

push が成功したら、ブラウザで https://yourprojectname.herokuapp.com/ にアクセスします。 Hello World! とブラウザに表示されると思います。

Web サーバに gunicorn を使う場合

Deploying Python Applications with Gunicorn | Heroku Dev Center を参考に、Heroku に gunicorn をインストールし、 Procfile というテキストファイルを作成します。

$ touch Procfile
$ echo "web: gunicorn hello:app --log-file -" >> Procfile

requirements.txt に gunicorn を追加します。

$ echo "gunicorn" >> requirements.txt
$ pip install -r requirements.txt

gunicorn でアプリが動くことを確認します。

$ gunicorn hello:app --log-file -

ブラウザで http://localhost:8000 にアクセスしてエラーなくコンテンツが返ってこれば成功です。

変更をコミットしたら Heroku リモートリポジトリに push します。これで Web サーバが gunicorn になります。

PostgreSQL の利用 (Heroku Postgres)

データベースの作成

Heroku Postgres という add-ons を使用することで PostgreSQL を使用できます。無料版であれば10,000レコードまでの制限があります。(無料版でもクレジットカードの登録が必要なことに注意。)

これも Heroku CLI から行います。

heroku addons:create heroku-postgresql:hobby-dev

Heroku Dashboard から作成したアプリの管理画面を開き、 Overview にて Installed add-ons に Heroku Postgres が Hobby Dev となっていれば無料版で PostgreSQL が作られています。(念のため Installed add-ons の横に $0.00/month と表示されていることも確認しましょう。)

データベースの管理

Heroku CLIpsql が使えます。

$ heroku pg:psql 

データベースへの接続

上記までの手順で自動的に Heroku 上の環境変数 DATABASE_URL に PostgreSQLURI がセットされるので、アプリからこれを PostgreSQL の接続情報として使うようにすればよいです。

ちなみに Python なら psycopg2 というライブラリを使うと簡単でした。

import os

import psycopg2

from flask import Flask, g


app = Flask(__name__)

# 環境変数から PostgreSQL の URI を取得
DATABASE_URL = os.environ.get("DATABASE_URL")

def get_pg_conn():
    """ PostgreSQL へ接続 """
    if not hasattr(g, "pg_conn"):
        g.pg_conn = psycopg2.connect(DATABASE_URL)
    return g.pg_conn


@app.teardown_appcontext
def close_pg_conn(error):
    """ エラーが発生したら PostgreSQL への接続を閉じる """
    if hasattr(g, "pg_conn"):
        g.pg_conn.close()


@app.route('/')
def hello_world():
    conn = get_pg_conn()
    cursor = conn.cursor()

    """
    何らかの PostgreSQL の処理
    """

    return 'Hello, World!'


if __name__ == "__main__":
    app.run()

以上で無料の範囲内で Heroku の PostgreSQL を使うことができます。

私はこれで旭川市のオープンデータを使ったアプリを無料でいくつか運用しています。

おまけ

PostgreSQL を無料版から有料版に移行する場合、新たに Heroku Postgres を追加し、アプリからのデータベース接続の向きを追加した Heroku Postgres へ向ける必要があります。

無料版 (hobby-dev) で10,000レコードを超えると、 Heroku から「7日間以内に上位プランに変更しないと INSERT 権限停止するよ(意訳)」とメールが来るのですが、 旭川市新型コロナウイルス感染症非公式オープンデータ が10,000レコードを超えてしまったので、一番安い hobby-basic ($9.00/month) に変更しました。

$ heroku addons:create heroku-postgresql:hobby-basic

追加した hobby-basic の環境変数を確認します。

$ heroku pg:info
=== HEROKU_POSTGRESQL_( ここが環境によって異なる )_URL
( 以下省略 )

一旦アプリをメンテナンスモードにしてから無料版データベースの内容を今回追加したデータベースへコピーします。

$ heroku maintenance:on
$ heroku pg:copy DATABASE_URL HEROKU_POSTGRESQL_( ここが環境によって異なる )_URL

環境変数 DATABASE_URL を hobby-basic の方の URI を返すようにしたいので、こちらをプライマリデータベースに昇格させます。

$ heroku pg:promote HEROKU_POSTGRESQL_( ここが環境によって異なる )_URL
$ heroku config:get DATABASE_URL
( 以下に hobby-basic の URI が表示されることを確認する )

最後にメンテナンスモードを解除して終了です。

$ heroku maintenance:off

スクレイピングで旭川市の新型コロナウイルス感染症に関する非公式オープンデータを作ってみた

新型コロナウイルス感染症に関する各関係者の努力には本当に頭が下がる思いで、コロナ禍が早く収束して欲しいと願うばかりなのですが、そんな中、各自治体の新型コロナウイルス感染症に関する情報を収集しようとした時に、機械的に処理しやすい形で情報が提供されていない現状があるという話を目にすることがあります。

現在各地のシビックテック団体や有志のエンジニア等により、地域ごとの情報提供サイトも立ち上げられています。

このような取り組みにおいて障壁となっているのが、国や自治体が公表している情報が機械判読しにくい、データの形式が揃っていないといった課題です。

自治体が共通の様式かつ機械判読性が高いデータ形式で公開することによって、より早くサイトが開発でき、更新もしやすくなるため、行政の公式情報を迅速に多くの人へ届けることにつながります。

新型コロナウイルス感染症対策のためのデータ公開支援 - Code for Japan

Code for Japan新型コロナウイルス感染症対策に関するオープンデータ項目定義書を公開していますが、各自治体の保健所は業務が逼迫していて、なかなかここまで手が回らないのが現実なのではないかと思います。

自分の住む旭川市もそうで、北海道全体の新型コロナウイルス感染症の情報については、北海道新型コロナウイルスまとめサイト北海道オープンデータポータルCSV形式のテキストファイルやJSON形式のWeb APIといった再利用しやすい形式で取得できるようになっていますが、旭川市公式ホームページで情報が公開されているものの、今のところオープンデータのような形では提供がされていません。

ただ、旭川市公式ホームページの新型コロナウイルス感染症に関する情報のページのHTMLはある程度規則性があり、スクレイピングすればうまく情報が拾えそうだったので、新型コロナウイルス感染症対策に関するオープンデータ項目定義書のうち、データセット No.01 陽性患者属性のみですが、CSV形式のテキストファイルを、非公式のオープンデータとしてダウンロードできるようにしたものを作りました。(旭川市単独のデータの需要があるのかは分かりませんが、少しでも誰かの何かの役に立てば……。)

ash-unofficial-covid19.herokuapp.com

https://ash-unofficial-covid19.herokuapp.com/012041_asahikawa_covid19_patients.csv

ただし、以下のデータ項目は旭川市公式ホームページからスクレイピングで取得することが困難だったため、北海道オープンデータポータルの陽性患者属性CSVから値を逆輸入?して取得しています。(旭川市公式ホームページに北海道の識別番号の記載があったので、北海道のオープンデータと紐付けできました。)

  • No.6 発症_年月日
  • No.10 患者_職業
  • No.11 患者_状態
  • No.12 患者_症状
  • No.13 患者_渡航歴の有無フラグ

スクレイピングPythonとBeautifulSoupを使い、DBはPostgreSQL、WebサーバはFlaskでHerokuにデプロイしています。Heroku Schedulerで1時間おきに情報を更新しています。ソースはGithubに上げています。

github.com

なお、本非公式オープンデータは個人で勝手に開発したもので、旭川市は一切関係ありません。また、内容の正確性は保証できませんのでご注意ください。

今回のスクレイピングについては、情報解析のための複製等に該当し著作権の問題は生じないと考えていますが、もし問題があればすぐに停止するつもりです。

情報解析のための複製等(第47条の7)

コンピュータ等を用いて情報解析(※)を行うことを目的とする場合には,必要と認められる限度において記録媒体に著作物を複製・翻案することができる。 ただし,情報解析用に広く提供されているデータベースの著作物については,この制限規定は適用されない。 ※情報解析とは,大量の情報から言語,音,映像等を抽出し,比較,分類等の統計的な解析を行うことをいう。

著作物が自由に使える場合 | 文化庁