KITA Eng.

北海道でサーバー技術者として歩み出したひとが綴るblog。

PyCon JP 2019でポスター発表しました——「Djangoで業務改善 〜子どもの福祉・教育に関わるNPOでの挑戦〜」

PyCon JP 2019でポスター発表しました。 諸般の都合によりがっつりポスターではなく、スライド資料をA3で8枚印刷したものを掲示しました。 (Slideshareで、掲示したスライド+αのものを公開しています)

www.slideshare.net

技術的なこと

今回の発表ではあまり技術的なバックグラウンドの事を盛り込んでいなかったので、いくつか技術的な部分をご紹介しておきます。

フロントエンド

Djangoのデフォルトテンプレートエンジンを使用し、CSSフレームワークとしてBootstrap3系を使用しています。
活動予定のカレンダー表示には、FullCalendarを使用しています。

プロジェクト構成

基本的に大まかな機能単位で、アプリ分割しています。
アプリを横断するようなものは、ひとまずcoreアプリにまとめる方針としています。
これらを一つのDockerコンテナの中で稼働させています。

.
├── Dockerfile
├── Dockerfile-development
├── bbs
├── chiebukuro
├── contribution
├── core
├── enquete
├── entrypoint.sh
├── gakubora_report
├── juken
├── kacomen
├── kacotabu
├── kacotam
├── manage.py
├── news_banner
├── requirements.in
├── requirements.txt
├── scheduler
├── sensei
├── static
├── sutasapo_report
├── tasks.sh
└── yurukichi

2017年の初期リリースから、適宜要望に応じて機能を追加し続けてきたので、だんだんカオスになってきています......。

RDB

RDBには、PostgreSQLを採用しています。特に深い採択理由はありません。

未来図の実現に向けて

未来図に向けて、ポータルの改修作業をちまちまと進めています。 その中で、単純に外に出せるcore部分を以下で順次公開しています。 github.com

ポータルで使っている、Djangoからチャットワークに通知したり、タスク追加したりするためのユーティリティは、django-chatworkとして、PyPiで公開しています。Issue、プルリク大歓迎です!!

pypi.org

github.com

FAQ

Q: 2015、2016年頃Djangoでゼロから作るのしんどくなかったですか?

A: Railsには浮気できない訳がありまして...、やり抜きました。bottle.pyを途中少し触って雰囲気をつかんだ感があります。

Q: インフラどうしてるんですか?

A: Cloud Garage の Develpper Assist Plan(開発者支援の無償利用プラン)を利用してきました(が、サービス終了を迎えるので、IDCFクラウドにお引っ越しします。月額3千円ほどでの構成になる予定)。

Q: サービスとして提供しているのですか?

A: 今はまだしていません。上述の未来図の通り後々にはしていきたいと思っています。コアのソースはOSSとして、カスタマイズや運用保守周りをサポートするようなモデルを想定しています。

Q: 実際どのくらい使ってますか?

A: 「もう無い頃には戻れない」というぐらいに使ってます。特に出欠集約周りは、自動のリマインドや月次集計レポート通知など人力から自動化された部分は、事務作業軽減に大きく寄与しています。

BIND9 ゾーンファイルの編集の罠

BIND9にて、ゾーン情報の編集を以下の流れでやった場合に、変更後の情報が反映されない事象が発生しました。

  1. 現在のゾーンファイル(zone.example.com)を別名にコピー(zone.example.com_copy)して、内容を編集(シリアル値はもちろんあげておく)
  2. 1.の作業から少ししてから、現在のゾーンファイル(zone.example.com)を編集して、BINDに再読み込みさせる
  3. 2.の作業の後に、現在のゾーンファイル(zone.example.com)を別名(zone.example.com_back)にして、zone.example.com_copyをzone.example.comにして、BINDに再読み込みさせる

この場合、2. においては、意図したとおりゾーンファイルの情報が再読み込みされるにも関わらず、3. の場合には、 再読み込みがされません(もちろんシリアル値は、大きくなっている)。

コマンドで上記を追うと、以下のようになる。

# cat zone.example.com
;$ORIGIN .
;$TTL 600           ; 10 minutes
$TTL 60             ; 1 minutes
@           IN SOA  ns.example.com. abuse.example.com. (
                2019090900 ; serial
                3600       ; refresh (1 hour)
                900        ; retry (15 minutes)
                604800     ; expire (1 week)
                86400      ; minimum (1 day)
                )
            NS  ns.example.com.
ns          A   172.16.3.1
test1           A   172.16.3.1


# dig +norec +short @172.16.3.1 test1.example.com
172.16.3.1
# dig +norec +short @172.16.3.1 test2.example.com   # 登録がないのでレスポンスなし
# dig +norec +short @172.16.3.1 test3.example.com   # 登録がないのでレスポンスなし


# cp -iva zone.example.com zone.example.com_copy
'zone.example.com' -> 'zone.example.com_copy'

# vi zone.example.com_copy 
# cat zone.example.com_copy 
;$ORIGIN .
;$TTL 600           ; 10 minutes
$TTL 60             ; 1 minutes
@           IN SOA  ns.example.com. abuse.example.com. (
                2019090910 ; serial    ; <-- 大きめに変更
                3600       ; refresh (1 hour)
                900        ; retry (15 minutes)
                604800     ; expire (1 week)
                86400      ; minimum (1 day)
                )
            NS  ns.example.com.
ns          A   172.16.3.1
test1           A   172.16.3.1
test3           A   172.16.3.1      ; <-- 追加した


# ls -la
total 16
drwxr-xr-x 2 root root 4096 Sep  9 18:01 .
drwxrwxr-x 3 root bind 4096 Sep  9 17:54 ..
-rw-r--r-- 1 bind bind  334 Sep  9 17:56 zone.example.com
-rw-r--r-- 1 bind bind  355 Sep  9 18:01 zone.example.com_copy

# vi zone.example.com
# cat zone.example.com
;$ORIGIN .
;$TTL 600           ; 10 minutes
$TTL 60             ; 1 minutes
@           IN SOA  ns.example.com. abuse.example.com. (
                2019090901 ; serial
                3600       ; refresh (1 hour)
                900        ; retry (15 minutes)
                604800     ; expire (1 week)
                86400      ; minimum (1 day)
                )
            NS  ns.example.com.
ns          A   172.16.3.1
test1           A   172.16.3.1
test2           A   172.16.3.1      ; <-- 追加した


# vi zone.example.com
# ls -la
total 16
drwxr-xr-x 2 root root 4096 Sep  9 18:02 .
drwxrwxr-x 3 root bind 4096 Sep  9 17:54 ..
-rw-r--r-- 1 bind bind  355 Sep  9 18:02 zone.example.com   # <-- こっちのタイムスタンプの方が新しい
-rw-r--r-- 1 bind bind  355 Sep  9 18:01 zone.example.com_copy


# rndc reload example.com    # 【リロード(1)】
zone reload queued

# dig +norec +short @172.16.3.1 test1.example.com
172.16.3.1
# dig +norec +short @172.16.3.1 test2.example.com   # <-- 追加したので出るようになった(意図通り)
172.16.3.1
# dig +norec +short @172.16.3.1 test3.example.com


# mv -iv zone.example.com zone.example.com_back
'zone.example.com' -> 'zone.example.com_back'
# mv -iv zone.example.com_copy zone.example.com
'zone.example.com_copy' -> 'zone.example.com'

# ls -la
total 16
drwxr-xr-x 2 root root 4096 Sep  9 18:04 .
drwxrwxr-x 3 root bind 4096 Sep  9 17:54 ..
-rw-r--r-- 1 bind bind  355 Sep  9 18:01 zone.example.com   # <-- mvで入れ替えたのでタイムスタンプが古いママ
-rw-r--r-- 1 bind bind  355 Sep  9 18:02 zone.example.com_back

 
# rndc reload example.com   # 【リロード(2)】
zone reload up-to-date
# dig +norec +short @172.16.3.1 test1.example.com
172.16.3.1
# dig +norec +short @172.16.3.1 test2.example.com   # <-- でないはずなのに出る
172.16.3.1
# dig +norec +short @172.16.3.1 test3.example.com   # <-- でるはずなのに出ない


# touch zone.example.com    # <-- タイムスタンプを新しくする
# ls -la
total 16
drwxr-xr-x 2 root root 4096 Sep  9 18:04 .
drwxrwxr-x 3 root bind 4096 Sep  9 17:54 ..
-rw-r--r-- 1 bind bind  355 Sep  9 18:05 zone.example.com
-rw-r--r-- 1 bind bind  355 Sep  9 18:02 zone.example.com_back
# rndc reload example.com   # 【リロード(3)】
zone reload queued


# dig +norec +short @172.16.3.1 test1.example.com
172.16.3.1
# dig +norec +short @172.16.3.1 test2.example.com   # <-- でなくなった(意図通り)
# dig +norec +short @172.16.3.1 test3.example.com   # <-- でるようになった(意図通り)
172.16.3.1

syslogには、以下のようにログが出力される。
【リロード(2)】のときには、「zone example.com/IN: loaded serial 2019090901」のように読み込んだファイルのシリアル値のログが出ていない。

Sep  9 18:03:50 localhost named[25597]: received control channel command 'reload example.com'
Sep  9 18:03:50 localhost named[25597]: zone example.com/IN: loaded serial 2019090901
Sep  9 18:04:52 localhost named[25597]: received control channel command 'reload example.com'
Sep  9 18:05:13 localhost named[25597]: received control channel command 'reload example.com'
Sep  9 18:05:13 localhost named[25597]: zone example.com/IN: loaded serial 2019090910

これは、ゾーンファイルの再読込について、ゾーンファイルの中身(シリアル値)が確認される前に、ゾーンファイルのタイムスタンプをBINDが確認しているためで、現在読み込まれているファイルのタイムスタンプよりも新しくなっていないと、再読込されない仕様なようです。

Tuning your BIND configuration effectively for zone transfers (particularly with many frequently-updated zones) - Performance

上のページの3段落目に、

the decision on whether or not the zone has changed is the serial number that is maintained in the zone's SOA record (although when a master reloads, it does also check the timestamp on the zone data file first, before checking the zone's SOA).

と書かれています。

普通に更新したら、ファイルのタイムスタンプは新しくなるだろうけど、上のように事前に用意しておいたファイルと置き換えるときには注意が必要なようです。

Cloud Garageの「接続許可(詳細設定)」でサーバーを守る

特定非営利活動法人Kacotamの内部用ポータルシステムの開発・運用等を担当させてもらっています(2017年5月からシステム稼働)。 このシステムのインフラ部分として、2017年9月からCloud Garage Dev Assist Program (DAP)での支援を受けています。 2017年7月から始まった新しいクラウドサーバーのサービスですが、これまで大きな障害等なく快適に使えています。

サービス開始当初は最小限の機能でのスタートでしたが、ついにCloud Garageにもインスタンス管理画面上で任意のポートに対する接続許可設定ができるようになっていました。

2018.10.05 新機能追加のお知らせ(接続許可 詳細設定)
従来のCloudGarageでは、未使用ポートへの不正アクセス防止のために、特定の開放ポートを選択して制限する方法をとっていましたが、今回のアップデートにより任意のプロトコル、ポート番号、送信元IPアドレスを指定できるようになりました。コントロールパネル上での簡単な操作で、より高いセキュリティレベルでの運用が可能になりましたので、ぜひご活用ください。 cloudgarage.jp

この機能ができるまでは、SSHの接続ポートを標準の22から変更している場合や、特定のIPからのみ全ポートを許可するなどをしたい場合は、インスタンスの管理画面上では、「全解放」にして、インスタンス内でiptablesなどを使って設定する必要がありました。

サーバー上での設定は、インスタンス作成直後や設定ミスで漏れが発生することもあるので、管理画面上からできるのは◎です

早速設定してみた

Cloud Garageのコントロールパネルはシンプルな作りなので、すぐに見つけられるような気がしますが、一応画面付きで順を追って紹介します。 一部、インスタンスの情報やIPアドレスの部分はマスクしています。

1. 変更するインスタンスの詳細画面を表示する

Cloud Garageのログイン画面からログインすると最初に表示される、インスタンスの一覧表示画面から、接続許可設定を変更したいインスタンス名をクリックする。 f:id:kacchan822:20181019133436p:plain

2. 接続許可設定のダイアログを表示する

インスタンス詳細画面の右下にある、「接続許可設定」の鉛筆アイコンをクリックする。 f:id:kacchan822:20181019133439p:plain

3. 接続許可「詳細設定」を行う

「かんたん設定」の範囲以外のポートを空けていたので、「全解放」のラジオボタンが選択されていました。 f:id:kacchan822:20181019133441p:plain

中央にある「詳細設定」のラジオボタンをクリックすると、ルールを入力できるようになります。
親切に説明があるように、ポートを範囲で指定したり、IPアドレスをブロック単位で指定することもできます。 f:id:kacchan822:20181019133444p:plain

入力ができたら、「更新」ボタンをクリックして保存します。

4. インスタンスの詳細画面で確認

正常に設定が完了すると、インスタンスの詳細画面の右下にある「接続許可設定」の部分に設定内容がリスト表示されています。 f:id:kacchan822:20181019133447p:plain

で、どうなの?

インスタンス内での設定は、iptablesのフロントとして、UFWを利用していて、ブロックしたパケットがあるとログに出るようにしています。ログを見ていると毎日毎日あらゆるところから、開いてもいない22や21、25への接続が来ていました(開いてないからつながりはしないけれど....)。
今回の「接続許可(詳細設定)」の設定後は、UFWのログに記録されるブロックパケットが皆無になりました。

Docker-composeを利用していると、ポートバインドの記述ミスで、UFWでは制限をかけていても、docker側のフォワードの関係でつながってしまう状況が発生してしまうこともあり得ます。
インスタンスに届く前で通信を制限してもらえることで、そういったミスの際のリスクを回避することができるので、ステキです!

(編集後記)

すっかりご無沙汰しておりました。
2017年から特定非営利活動法人Kacotamにて、システム開発室室長の銘を受けていました。
2017年春に、団体WEBページのリニューアル、内部向けポータルサイトの稼働などがあり、わたわたとした1年でした。
これまでは、僕自身を含め、Kacotamの主たる活動の学習支援活動と兼務でのメンバーばかりでしたが、最近開発室専従のメンバーが加わり始めました。
子どもの福祉に関わるNPOで、システム開発室を自前で持っているところはないと思うので、少しでも情報発信して行けたらなと考えています。
(後々専用のTech Blogができるかもですが、)しばらくは、Kita Eng. からお伝えしていきます。関連記事には、Kacotam-Si-Teamのタグをつけるようにしていきます。

Django Class-based views でCSVダウンロードページの実装ではまったこと

公式ドキュメント Outputting CSV with Django | Django documentation | Django とか、GoDjangoの記事 Download CSV files via CSVResponseMixin - GoDjango とかあるからさくっと行けるだろうと思ったら、意外な落とし穴があった。

ダウンロードされるCSVファイルのファイル名をレスポンスヘッダーの"Content-Disposition"に入れてあげるのですが、Chromeさんだと特に何も考えず、

GoDjangoにある以下の書き方で、日本語名でも全く問題なく行ってくれました。

response = HttpResponse(content_type='text/csv')
cd = 'attachment; filename="{0}"'.format(self.get_csv_filename())
response['Content-Disposition'] = cd

が残念ながら、Firefoxさんだとファイル名を受け取ってくれません。 Google先生に問い合わせると、先人がいらっしゃいました(PHPでの実装だけど)。

scientre.hateblo.jp

比較的最近のブラウザでは RFC 2231 形式に対応しているので、通常はこの形式を使えば問題は少ないだろう。

ということなので、 RFC 2231 - MIME Parameter Value and Encoded Word Extensions: Character Sets, Languages, and Continuations を読んだつもりになって、「python rfc2231」とかってググると、 19.1.14. email.utils: 多方面のユーティリティ — Python 3.6.5 ドキュメント 使っとけって出てくるので、使ってみたら無事解決。

たぶん使いまわしがきくであろうMixiにしておいてみた。

UTF-8CSVをうまくExcelさんに読んでもらうためには、BOM付UTF-8にしないといけないらしく、それでも微妙にはまった(というのも↑Mixinは対応)。エンコーディング問題というのはどこへ行っても頭を悩ませる種ですね。。。

IDCFクラウドのオブジェクトストレージをdjango-storagesから使ってみた。

最近はオブジェクトストレージをうまく使って、静的ファイルを配信するのが大きな潮流な模様。

静的ファイルのサイズが増え続けたり、ミニマムスタートでサービス立てたり、アプリケーションサーバー側をステートレスで必要に応じてスケーリングしたり...

オブジェクトストレージ殿は、そんな要求たちを一気にやっつけてくれるえらい奴だと、思い込んでおります。(全然実務の方ではご縁になることが、ややしばらくなさそうなんですけど...)

こないだ300円サーバーを作ってみたIDCFクラウドにもオブジェクトストレージのサービスがあって、50GBまでなら月額無料、転送もOUTトラフィック10GBまでは月額無料だというので、こりゃ試してみるのにはちょうどよさげだと思って、試してみることにしました。

www.idcf.jp

ちょうどDjangoさんを弄っていたところだったので、Djangodjango-storagesからS3のようにさくっと使えるだろうと思って始めたのですが... pypi.python.org

意外と情報がなくって微妙に苦戦したのでメモしておきます。S3互換のAPIが提供されているとはいえ、エンドポイントとかがS3とは違うわけなので、ライブラリにお任せする時にはちょっと設定をしないといけないのですね(あたりまえ)

IDCFクラウドの方は、コンパネからAPIユーザーを作って、バケットを作っておいてあげればOKです。 デフォルトだと非公開バケットができるので、バケットの詳細設定からパブリックしておく必要がありますね。

設定で必要になる情報は、APIユーザーを選択したときに最初に表示される画面(以下)に集約されています。 f:id:kacchan822:20161010224606p:plain

Djangoの方の設定は、基本的に公式ドキュメント通りで、以下のような感じになりました。 bucket_nameに作成したバケット名を入れましょう。idcfクラウドは基本的にサブドメイン形式な模様。

setting.py

INSTALLED_APPS = [
    ...

    'storages',

    ...
]

 ...

STATIC_URL = 'https://bucket_name.ds.jp-east.idcfcloud.com/'
STATICFILES_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
AWS_STORAGE_BUCKET_NAME = 'bucket_name'

 ...

これで、あとは、アクセスキーとシークレットキーを環境変数で渡してあげればOKだぜ!と思って

python manage.py collectstatic -n

ってやったら怒られました。つながんねーと的なことでした。Google大先生に尋ねたところ、 masato.github.io

こちらが大変参考になりました。~/.botoファイルに以下のような内容を書いてあげればよろしいよと。

~/.boto

[Credentials]
aws_access_key_id = xxxxxxxxxxxxxxxxxxxxxxxx
aws_secret_access_key = yyyyyyyyyyyyyyyyyyyyyyyyyy

[s3]
host = ds.jp-east.idcfcloud.com

これをつくってから、もう一度

python manage.py collectstatic -n

やったら、一生懸命あげる感じの雰囲気がでました。-n外してやったら確かにIDCFクラウドのオブジェクトストレージ上に保存されて、静的ファイルがそちらから配信されました。

ということで、とりあえずお試し完了です。


編集後記

アクセスキーとシークレットキーは環境変数経由で直接Djangoの設定ファイルへ渡せるっぽいけど、hostの方の設定はどうにかなるのだろうか。Djangoさんやら関係パッケージ周りの仕組みが未知数な部分がありすぎて謎だ。