読者です 読者をやめる 読者になる 読者になる

akiyoko blog

akiyoko の IT技術系ブログです

ベスト・オブ・Django本!

Django

この投稿は 「Django Advent Calendar 2016 - Qiita」 の 5日目の記事です。


「Django」を勉強するときに一番困るのは、やっぱり 「Django本がない」 問題ですよね?


実際、Django 初心者や初級者のエンジニアがいざ本格的に勉強しようと思っても、Django の専門書が圧倒的に少ないという問題が大きな壁として立ちはだかっているのではないでしょうか。


あっても古いとか。

開発のプロが教える標準Django完全解説―Webアプリケーションフレームワーク (デベロッパー・ツール・シリーズ)

開発のプロが教える標準Django完全解説―Webアプリケーションフレームワーク (デベロッパー・ツール・シリーズ)

2008年発売って、Django のバージョンは 1.0 ですよね。
さすがに情報が古すぎます。。


あと、Django に割かれているページ数が少ないとか。

Pythonプロフェッショナルプログラミング第2版

Pythonプロフェッショナルプログラミング第2版

Django について書かれた章がありますが、それでも 20ページ程度しかありません。。


「もっと新しくて、ちゃんと全体を網羅している Django の専門書はないの??」
私も常々、こんな疑問を抱いていました。



書籍の充実した Ruby や PHP と比べて、Python のフルスタック Webアプリケーションフレームワークに関する書籍が少ないという問題が、Django があまり流行ってない大きな原因になっているのでは??というのが、私の率直な印象です。




で、見つけましたー!!


私が自信を持ってオススメできる ベスト・オブ・ Django本が「Two Scoops of Django: Best Practices for Django 1.8」 です!

Two Scoops of Django: Best Practices for Django 1.8

Two Scoops of Django: Best Practices for Django 1.8


残念ながら全編英語の本ですが(*1)、設計のベストプラクティスから細かな Tips まで詳細に分かりやすく解説されている Django の専門書です。


対象者は、初心者というよりも、チュートリアル を読み終えたくらいの初級者から、現場で Django を何年か使っている中級者くらいを想定していると思われます。
専門書と言っても堅苦しさは全く無く、頻繁にアイスクリームショップの話が引き合いに出されたりと、ポップな世界観(!)を醸し出しています。


著者は Daniel Roy GreenfeldAudrey Roy Greenfeld の二人のエンジニアで(*2)、プロジェクトで Django を使い倒している超ベテランです。これまで、Django 1.5, 1.6 向けの Two Scoops of Djangoシリーズ を出版してきた経歴もあるので、本の内容の充実度ぶりは半端ないです。


本のタイトル「Two Scoops of Django(Django の 2つのスクープ)」の由来は、「二人がお届けする Django の最新情報(スクープ)」という意味と、Django をアイスクリームに見立てて「二すくい(スクープ)のアイスクリーム」という意味を掛けているんじゃないかと思われます。




ちなみに、現時点の Django の最新バージョンは 1.10 ですが、この本の対象バージョンは Django 1.8 です。しかしながら、1.8 は LTS(長期サポートバージョン)なので、少なくとも 2018年4月までは本家からセキュリティとバグフィックスが提供されることになるため、現場で使う場合も安心です。

f:id:akiyoko:20161122010842p:plain
Download Django | Django より引用)


著者は、次の「Two Scoops of Django」シリーズはバージョン 2.2 のリリース以降と言っているので、2019年まではこの「Two Scoops of Django: Best Practices for Django 1.8」にお世話になりそうです。なので、この本を買って 2019年まで読み倒すのがよいかと思います。




 

目次

ざっと内容を紹介しますが、詳細には触れていませんので興味がある内容が一つでもあれば実際に読んでみてもらえればと思います。

著者曰く、どの章から読み始めても問題ないそうです(実際、問題ありません)。


 

第1章:Coding Style(コーディングスタイル)
  • p.1〜
  • PEP8 に準拠すべし
  • OSSプロジェクトは1行79文字、その他のプロジェクトは1行99文字制限に
  • 明示的な相対インポートを使うべし
  • Django コーディングスタイル およびその他のガイドラインについて
第2章:The Optimal Django Environment Setup(最適なDjango環境設定)
  • p.13〜
  • 開発環境のセットアップについて
  • 開発環境と本番環境で別々のデータベースを使っている場合は要注意 *3
  • pip と virtualenv を使おう。virtualenvwrapper もオススメ
  • コードを Git や Mercurial でバージョン管理しよう
  • Vagrant + VirtualBox、Docker などを使って開発環境を仮想化する
第3章:How To Lay Out Django Projects(Djangoプロジェクトをどのようにレイアウトすべきか)
  • p.21〜
  • プロジェクトレイアウトのベストプラクティス(リポジトリルート/プロジェクトルート/コンフィグレーションルート)
  • virtualenv の格納場所はプロジェクト外に *4
  • startproject の代わりに、cookiecutter-django コマンドを使って究極の Djangoテンプレートプロジェクトを作成しよう
第4章:Fundamentals of Django App Design(Djangoアプリケーション設計の基本)
  • p.33〜
  • Django app(アプリケーション)を理解しよう。 *5
  • それぞれのアプリケーションは一つのタスクに集中すべき(一文で説明できるようにできるだけ小さく設計すべき)
  • アプリケーション名はメインとなるモデルの複数形にすべし *6
  • アプリケーションに含めるべきモジュール群(とネーミング)
第5章:Settings and Requirements Files(SettingsモジュールとRequirementsファイル)
  • p.41〜
  • 「SECRET_KEY」などのシークレットキー(*7)を除く全ての設定はバージョン管理下に置くべき
  • シークレットキーの設定には環境変数を使う(付録Eを参照)
  • アンチ local_settings.py パターンとして、各環境に合わせた Settingsモジュール(base.py/local.py/staging.py/test.py/production.py)を用意
  • Settingsモジュール内のファイルパスの書き方について
第6章:Model Best Practices(モデルのベストプラクティス)
  • p.63〜
  • django-model-utils の TimeStampedModel や django-extensions はどのプロジェクトでも便利に使える
  • モデルが5個までに収まるようにアプリケーションの大きさを設計する
  • 3種類のモデルの継承方法(abstract base classes, multi-table inheritance, proxy models)のうちどれを使うべきか *8, *9
  • データベースのマイグレーション方法について
  • テーブル設計で非正規化をする前にキャッシングを検討しよう
  • null=True, blank=True の使いどころ
  • models.field.GenericForeignKey はなるべく使わないようにしよう
  • モデルの _meta API について
  • モデルマネージャについて
  • ファットモデル志向(データ処理やビジネスロジックはモデルに集約)
  • モデルがファットになりすぎないように、Mixin やステートレスなヘルパー関数に分離しよう
第7章:Queries and the Database Layer(クエリとデータベース層)
  • p.81〜
  • ビューで単一のオブジェクトを取得する場合は get() の代わりに get_object_or_404() を使う *10
  • ObjectDoesNotExist はどのモデルでも共通で使える
  • get() で複数のオブジェクトが抽出されてしまう場合は MultipleObjectsReturned で判定
  • 遅延評価を理解しよう。長いクエリは読みやすいように分割しよう
  • Django 1.8 から UPPER, LOWER, SUBSTR などの関数が使えるように *11
  • 生の SQL は出来る限り書かないようにする *12
  • デフォルトのトランザクションはクエリごと *13
第8章:Function- and Class-Based Views(関数ベースおよびクラスベースのビュー)
  • p.95〜
  • 関数ベース、クラスベースのビューのどちらを使うべきか? *14
  • URLConf にロジックを書かない(ビューやモデルを使い回せるように分離)
  • アプリケーションレベルの namespace を使うと便利
  • URLConf のビューへの参照は文字列で書かない
第9章:Best Practices for Function-Based Views(関数ベースビューのベストプラクティス)
  • p.109〜
  • 再利用できない、クラスベースビューのように継承が使えない、のが難点
  • HttpRequest を引数にしたヘルパー関数やデコレータを使うことで、ビューをシンプルにできる
第10章:Best Practices for Class-Based Views(クラスベースビューのベストプラクティス)
  • p.117〜
  • 継承構造がややこしいのが難点
  • Mixin を利用しよう *15
  • 代表的な GCBV(generic class-based view)の紹介 *16
  • CBV + ModelForm, CBV + Form の例
  • django-braces の Mixin は GCBV の足りない部分を補完してくれるので便利
第11章:Form Fundamentals(フォームの基本)
  • p.137〜
  • ModelForm を使おう
  • 外部から入力された値はフォームでバリデーションすべし
  • データの更新を伴うリクエストには CSRF対策を施そう
  • Ajaxリクエストには、CSRF対策を施すか、HTTPヘッダに「X-CSRFToken」を設定する
  • django-braces の ModelForm Mixin は便利 *17
  • form.is_valid() の仕組み
第12章:Common Patterns for Forms(フォームのよくあるパターン)
  • p.149〜
  • フォーム関連の便利パッケージについて *18
  • clean() には複数フィールドにまたがる相関チェックを書く
  • clean(), clean_xxx() には、データベースに永続化されたデータを使ったバリデーションを書いても OK
  • Model とほぼ同じフィールドを Form に加えるときは、コピペせずに ModelForm の __init__ で self.fields を参照すればよい
第13章:Templates: Best Practices(テンプレート:ベストプラクティス)
  • p.167〜
  • テンプレートファイルは templates/ にひとまとめにしよう
  • 二層構造にする。それぞれのアプリケーションごとにベースのレイアウトを変えたい場合は(base.html を継承した)三層構造にする
  • "Flat is better than nested"
  • テンプレートレイヤでは必要以上にクエリや処理が実行されないように注意
  • テンプレートは「データを表示する」だけにする
  • ループ内で不必要なクエリが発行されないように select_related を使う *19
  • テンプレートの継承を理解しよう
  • テンプレートファイルやブロックなどの名前には「-」ではなく「_」を使う
  • 他のテンプレートファイルから include されるテンプレートファイル名は「_」で始める
  • CBV を利用した場合、「object_list」という暗黙の変数名が使える
  • テンプレート内では URL のパスはハードコードせずに url タグを使う
  • テンプレートのデバッグには TEMPLATES の「string_if_invalid」オプションの設定を使うと便利
  • 404.html と 500.html のエラーページは最低限用意しておこう
第14章:Template Tags and Filters(テンプレートタグとフィルタ)
  • p.191〜
  • フィルタは単なる関数(ただし引数は二つまでという制限あり)
  • テンプレートタグはデバッグしづらい
  • カスタムテンプレートタグのモジュール名は <app name>_tags.py とする
第15章:Django Templates and Jinja2(DjangoテンプレートとJinja2)
  • p.197〜
  • Django 1.8 から Django template language (DTL) 以外に Jinja2 をサポート
  • DTL と Jinja2 のどちらを使うべきか *20
  • ディレクトリごとに適用するテンプレートエンジンを替えることも可能
第16章:Building REST APIs(REST APIの構築)
  • p.207〜
第17章:Consuming REST APIs(REST APIの使用)
  • p.225〜
第18章:Tradeoffs of Replacing Core Components(コアコンポーネントを入れ替える場合のトレードオフ)
  • p.237〜
第19章:Working With the Django Admin(Django管理サイトを動かす)
  • p.243〜
  • Django 管理サイトはエンドユーザ向けのものではなく、管理者向けのもの
  • 一覧表示を見やすくするために、モデルに __str__() メソッドを用意しよう *21
  • 一覧に表示するフィールドは list_display で宣言する
  • 詳細表示をカスタマイズする場合は、ModelAdmin を継承したクラスを用意
  • どうしても管理サイトの見た目をカスタマイズしたい場合は https://github.com/sehmaschine/django-grappelli:titledjango-grappelli などのスキンを使う
第20章:Dealing with the User Model(Userモデルを扱う)
  • p.255〜
  • Django 1.5 以降でユーザクラスを設定で変更できるようになったため、ユーザクラスを取得するには get_user_model() を使う
  • ユーザモデルに対して一対多・一対一・多対多関連のフィールドを作成する場合は、settings.AUTH_USER_MODEL を使う
  • ユーザモデルをカスタマイズするには django-authtools が便利
  • ユーザモデルにカスタムフィールドを追加するための 3つのオプション *22
第21章:Django's Secret Sauce: Third-Party Packages(Djangoの秘伝のソース:サードパーティのパッケージ)
  • p.263〜
第22章:Testing Stinks and Is a Waste of Money!(疑わしいものをテストしよう!テストはお金の無駄!)
  • p.283〜
第23章:Documentation: Be Obsessed(ドキュメンテーションせずにはいられない)
  • p.301〜
  • ドキュメントは reStructuredText(RST)形式 で書こう *23
  • .rstファイルは Sphinx で各種フォーマットに変換して出力する
  • どんなドキュメントを作るべきか?
第24章:Finding and Reducing Bottlenecks(ボトルネックの発見と解消)
第25章:Asynchronous Task Queues(非同期タスクキュー)
  • p.319〜
  • Celery, Redis Queue, django-background-tasks のどれを使うべきか?
  • タスクはビューのように小さくせよ
第26章:Security Best Practices(セキュリティのベストプラクティス)
  • p.327〜
  • XSS、CSRF、SQLインジェクション、クリックジャッキング、TLS/HTTPS サポート、HTMLタグの自動エスケープなどが標準装備
  • SECERT_KEY の取り扱いに注意すべし(バージョン管理ツールの管理下にしないように)
  • HTTPS/SSL 化するには django.middleware.security.SecurityMiddleware を使う(ただし、static, media は含まれないので注意)
  • 標準ライブラリの pickle には気をつけよう
  • cookieベースのセッションは使わない
  • クレジットカード番号やパスワードなどのセキュアなデータ入力フィールドを利用する際の注意点 *24
  • 強度の高いパスワードを生成するには django-passwords, django-autoadmin などを利用する
  • アップロードファイルは python-magic でファイルヘッダを確認する
  • Django管理サイトへのIPアドレス制限をする場合は Middleware で判別
第27章:Logging: What’s It For, Anyway?(ロギング:それって何のため?)
  • p.355〜
  • 各ログレベル(CRITICAL/ERROR/WARNING/INFO/DEBUG)の用途
  • ロガーは再利用せずにモジュールごとに用意する
  • Logger.exception() について
  • logutils パッケージが便利
第28章:Signals: Use Cases and Avoidance Techniques(シグナル:ユースケースと回避テクニック)
  • p.365〜
  • シグナルは同期・ブロッキングなので、パフォーマンスを考慮すべし
  • 複数モデルを操作する場合や save後にキャッシュを無効化したい場合などはシグナルを使ってよい
  • models.Manager のメソッドやフォームの cleanメソッドで置き換えられないか検討する
  • 単一モデルを操作している場合は saveメソッド内に書けないか検討する
第29章:What About Those Random Utilities?(ちょっとしたユーティリティはどうしたらいい?)
  • p.371〜
  • 汎用的な共通モジュールは coreアプリケーションに配置しよう
  • 各アプリケーション直下にヘルパーモジュール utils.py(helpers.py)を
  • Django に内蔵されているヘルパーを利用しよう *25, *26, *27
第30章:Deployment: Platforms as a Service(デプロイ:プラットフォーム・アズ・ア・サービス)
  • p.387〜
第31章:Deploying Django Projects(Djangoプロジェクトのデプロイ)
  • p.395〜
第32章:Continuous Integration(継続的インテグレーション)
  • p.411〜
第33章:The Art of Debugging(デバッグの技術)
  • p.417〜
第34章:Where and How to Ask Django Questions(Djangoに関する質問をする場所と方法)
  • p.427〜
第35章:Closing Thoughts(最後に)
  • p.431〜
  • Django 2.2 がリリースされるまでは次のシリーズは書かない
  • Django 1.8 は LTE なので、しばらくはこの本が使えるでしょう


 

まとめ

「Two Scoops of Django」の著者は、Django を理解するには、フォーム(Form)、モデル(Model)、クラスベースビュー(CBV)について理解することが重要だと言っていますが、私は特に、モデルと Django ORM 周りについての理解がネックになると考えています。


「Two Scoops of Django」は全編英語で、英語が苦手な人には取っ付きにくいかもしれませんが、どの章から読み始めてもよいという性質と堅苦しくないポップな雰囲気から、Django 初級者にピッタリな本だと思います。ポップなタッチながらも、Django の実戦的なノウハウが一から十まできめ細やかに書かれていて、Django アプリのデベロッパーにとって非常に貴重な本に仕上がっています。


英語が苦手でない人には絶対オススメなので、是非読んでみてください!!



明日は、felyce さんの 6日目の記事です。よろしくお願いします。



 

英単語

最後に、「Two Scoops of Django」本に出てきた英単語をピックアップしました。
読むときのお役に立てば。


p.xxxv vet : 吟味する

p.xxxv distill : 抽出する

p.xxxv suffice : 十分である

p.xxxvi errata : 正誤表

p.1 abbreviate : 省略する

p.3 accommodate : 適応する、同意する

p.3 provision : 規定、供給

p.8 It goes without saying : 〜は言うまでもない

p.13 optimal : 最適な

p.13 pitfall : 落とし穴

p.13 identically : 完全に同じように

p.14 without a hitch : 問題なく

p.15 pre-populate : 自動入力する

p.15 familiarize : 習熟させる

p.26 intentional : 故意の

p.34 truncated : 省略された

p.34 in essence : 要するに

p.34 moderate : 適度な

p.35 envision : 心に思い描く

p.36 convention : 慣習、慣例

p.36 dull : つまらない

p.36 correspond with : 〜と一致する

p.36 discouraged : 推奨されない

p.37 when in doubt : 判別がつかないときは

p.37 a modicum of : 少量の

p.41 stay away from : 〜を避ける

p.42 purposefully : 意図的に

p.48 substantially : 実質的に

p.48 without hesitation : 気兼ねなく

p.63 think things through : じっくり物事を考える

p.63 down the road : やがて

p.63 sound : 堅固な、安定した

p.64 ramification : 予期せぬ問題

p.64 sloppy : ずさんな

p.64 derived : 生成された

p.65 concrete : 具象的な

p.65 overlap : 重複部分

p.65 inadvertent : うっかりした

p.65 substantial : 相当の

p.65 traverse : 横断する

p.67 nasty : 不快な

p.67 propagation : 伝播

p.67 aptly : 適切に

p.68 unwieldy : 手に負えない

p.68 bring someone to heel : 〜を従わせる

p.69 prematurely : 時期尚早に

p.69 panacea : 万能薬

p.73 unconstrained : 拘束されない

p.73 akin : 同種の

p.73 comforted : 安心した

p.78 infer : 推察する

p.79 judiciously : 思慮深く

p.81 consistently : 一貫して

p.81 quirk : 奇癖

p.81 cartwheel : 車輪

p.81 unconstrained : 制約されない、とらわれない

P.83 legible : 読みやすい

p.84 mitigate : 和らげる、軽減する

p.84 lean on : 〜に頼る

p.85 shudder : 身震いする

p.86 comparison : 比較

p.86 under the hood : 内部で

p.87 decent : 適切な

p.87 drastically : 抜本的に

p.88 approximate : 〜に近い

p.89 practitioner : 実行者

p.89 acronym : 頭字語

p.89 overhaul : 〜を徹底的に点検する

p.89 modernize : 近代化する

p.90 monumental : (記念碑のように)巨大な

p.90 embarrassing : 厄介な

p.90 crop up : 不意に起こる

P.92 downside : 否定的側面

p.96 err on the side of : 〜し過ぎて失敗する(傾向にある)

p.97 yell : 怒鳴る

p.98 argue : 異議を唱える、主張する

p.98 steer clear of : 〜を避ける

P.98 stick to : 〜に忠実である

p.101 hackery : ハッカー行為

p.101 elaborative : 入念な

p.101 automagically : 魔法のごとく自動的に

p.103 collide : 衝突する

p.104 prevalence : 普及、行き渡ること

p.104 tangible : 明らかな、具体的な

p.105 come into play : 動き始める

p.106 when it comes down to it : いざというときには、ここ一番というときには

p.106 algebra : 代数

p.107 one-off : 一回限りの、単発の

p.108 annoy : 困らせる、イライラさせる

p.108 exploit : 〜を利用する、十分に引き出す

p.108 adhere to : 〜を支持する、忠実に守る

p.109 come at the expense of : 〜を犠牲にして成り立つ、〜という代償で手に入る

p.110 cognitive overload : 認知的過負荷

p.110 arbitrary : 任意の

p.111 in the meantime : その間に

p.113 for once : 今回に限り

p.113 parlance : 専門用語

p.113 out of necessity : 必要に迫られて

p.113 to the point of : 〜するくらいまで

p.113 ubiquitous : 至る所にある

p.115 astute : 抜け目のない

p.117 advent : 出現、到来

p.117 out of the box : 難しい設定などは一切なしで

p.117 address : 対処する、取り組む

p.118 along the line of : 〜に従って

p.122 constrain : 制約する

p.128 catch : 落とし穴、わな

p.128 wire into : 〜に配線する

p.130 queue up : 列を作る

p.131 to recap : 要点をまとめると

p.131 intention : 意図

p.133 readily : すぐに、容易に

p.135 straight-forward : 簡単な、分かりやすい

p.135 in essence : 要するに

p.137 trivial : ささいな

p.137 anguish : 苦悩

p.137 albeit : 〜ではあるが

p.140 alteration : 変更

p.140 idempotent : 冪等の

p.141 capability : 能力、機能

p.141 exempt : 免除された

p.142 in conjunction with : 〜と連動して

p.144 iterate : 〜を反復する

p.144 coerce : 〜を強要する

p.146 streamline : 簡素化する

p.149 extensively : 広範囲に渡って

p.149 explicitly : 明確に

p.152 corruption : (データの)破壊

p.154 corruption : 〜を除いて

p.159 fancy : 手の込んだ

p.161 gross : 不作法な、気持ち悪い

P.167 constrain : 〜を制約する

p.168 tier : 層

p.169 consistent : 矛盾のない、一貫した

p.169 distinctive : 独特の

p.171 eloquently : 雄弁に

p.171 inefficiency : 効率が悪いこと

p.171 redeemable : 商品と交換できる

p.172 redeem : 商品と引き換える

p.173 breakdown : 分析結果

p.173 redemption : 回収

p.177 implied : 暗黙の

p.178 intensive : 集中的な

p.179 consumption : 消費

p.179 distract : 〜の気を散らす

p.179 bluntly : 単刀直入に

p.179 realign : 再編成する

p.185 mnemonic : (記憶を助ける)短い語句

p.186 intuitive : 直感的な

p.189 draw : 呼び物、引き付けるもの

p.189 carry away : 心を奪う

p.189 worse yet : さらに悪いことには

P.189 a blessing in disguise : 災い転じて福となす

p.190 bulky : 大きい

p.190 not to mention : 〜は言うまでもなく

p.191 trait : 特徴

p.191 prone : 〜しがちな

p.191 abuse : 〜を乱用する

p.193 unbearably : 我慢できないほど

p.195 fury : 激怒

p.196 supposedly : おそらく

p.196 chagrin : 残念さ

p.196 obscure : 目立たない

p.196 contention : 主張、論争

p.197 syntactical : 構文の

p.198 First off : 最初に

p.198 advent : 出現

p.198 harmoniously : 平和に

p.199 mitigate : 和らげる

p.200 incorporate : 〜を組み込む

P.237 hype : 誇大広告

P.238 fad : 流行

P.241 advocate : 支持する、推奨する

p.243 mess around with : 〜をいじり回す

p.243 revoke : 〜を無効にする、〜を取り消す

p.243 surreal : 非現実的な

p.250 oddly : 変に

p.251 venerable : 尊敬すべき、非常に古い

p.251 up-and-comer : 新人

p.252 arcane : 難解な

p.252 account for : 〜を構成する、〜の主要因である

p.252 idiosyncrasy : 特異性

p.252 invariably : いつも、常に

p.252 mind-numbing : 極めて退屈でつまらない

p.255 significantly : 著しく、かなり

p.255 radical : 抜本的な、急進的な

p.257 timid : 臆病な

p.257 intrusive : 煩わしい

p.257 in place : 適所に

p.259 bare-bones : 必要最低限の

p.259 slate : 候補者リスト

p.259 sane : 健全な

p.260 unobtrusive : 控えめな

p.261 dedicated : 専用の

p.301 That being said : そうは言っても

p.301 can't help but : 〜せずにはいられない

p.305 infrequently : まれに

p.305 tangential : 無関係の、脱線した

p.309 snappy : きびきびした

p.309 excessive : 必要以上の、過度の

p.310 invaluable : すこぶる有益な

p.312 noticeable : 目立つ

p.320 pitch in : 協力する

p.320 supervision : 監督、管理

p.320 Arguably : 議論の余地はあるが、ほぼ間違いなく

p.320 intensive : 集約的な

p.321 overkill : やり過ぎ

p.321 footprint : 足跡、専有面積

p.321 rules of thumb : 経験則

p.322 quirk : 奇癖

p.322 portability : 可搬性

p.323 underlying : 下部の、内在する

p.323 counter-productive : 非生産的な

p.328 malicious : 悪意のある

p.328 sniff : 盗聴する

p.328 up for grabs : 容易に手に入る

p.329 reputable : 信頼できる

p.332 expedite : 〜を早める、〜を迅速に処理する

p.332 malignant : 悪意のある

p.334 beware of : 〜に気をつける

p.335 falsify : 改ざんする

p.335 impersonate : 〜になりすます

p.336 transmit : 送信する

p.338 coerce : 〜を強要する

p.338 grave : 重大な

p.341 arbitrary : 任意の

p.341 be of concern : 心配事である

p.341 adequate : 適切な

p.342 incorporate : 〜を組み込む

p.343 paraphrased : 言い換える

p.343 trivial : ささいな

p.344 fine-grain : 微粒子の

p.344 in conjunction with : 〜と連動して

p.345 concealed : 隠れた

p.345 devastating : 壊滅的な

p.347 venerable : 尊敬すべき

p.347 punctuation : 句読点

p.347 pledge : 誓う

p.348 put up : 用意する、立ち上げる

p.348 overwhelm : 圧倒する、〜をひっくり返す

p.348 snap decision : 即断、見切り発車

p.348 ill-advised : 軽率な

p.349 bonded : 保証付きの

p.351 compromise : 譲歩

p.351 obfuscate : 分かりにくくする

p.351 cumbersome : 扱いにくい

p.352 nigh : ほとんど

p.352 corresponding : 同様の

p.353 diligence : 不断の努力

p.355 What's it for? : それは何に使うの?

p.355 once in a while : たまに

p.355 meticulously : 慎重に

p.356 go-to : 頼りになる

p.356 unpredictable : 予測できない

p.356 might as well : 〜してもいい

p.356 fine-tune : 微調整する

p.356 account for : 〜を構成する

p.356 introspection : 内省

p.357 catastrophic : 壊滅的な

p.357 sparingly : 控えめに

p.358 intruder : 侵入者

p.360 sprinkle : ちりばめる

p.360 debt : 負債

p.360 go overboard : やり過ぎる

p.360 clutter : 〜で溢れさせる

p.363 reduction : 減少

p.365 last resort : 最後の手段

p.365 knot : からまる

p.365 hairball : 毛玉

p.365 untangle : 〜のもつれを解く

p.365 dispatch : 〜を送り出す

p.365 obfuscation : 難読化

p.366 inversion : 逆転

p.370 delve into : 掘り下げる

p.371 general-purpose : 用途の広い

p.372 inevitably : 必然的に

p.372 Synonymous with : 〜と同じ意味の

p.373 brobdingnagian : 巨大な

p.374 indispensable : 不可欠の

p.385 a plethora of : 過多の

p.385 tangle : 混乱

p.431 ground : 領域

p.431 starter : 初心者

p.431 as for : 〜に関しては

p.431 genuinely : 心から

p.431 omission : 省略、手抜かり

*1:英語は易しめだと思います

*2:二人は 2013年末に結婚しています。 https://www.pydanny.com/i-married-audrey-roy.html また、cookiecutter-django のコアコミッターとしても有名です。 https://github.com/audreyr/cookiecutter

*3:データベースごとにフィールドタイプの扱い方が違っていたりする

*4:例えば「~/projects/」あるいは「~/.envs/」など。virtualenvwrapper を使っているのであれば「~/.virtualenvs/」など

*5:「Django apps are small libraries designed to represent a single aspect of a project.」とのこと。

*6:推奨しないが、単語と単語の間にはアンダースコアを使ってもよい

*7:AWS・Stripe などの APIキー、OAuth トークンなども含まれる

*8:「abstract base classes」は Metaクラスに「abstract = True」を記述した抽象ベースクラスを継承する方式、「multi-table inheritance」は OneToOneField で複数のテーブルを一対一関連させる方式、「proxy models」は Metaクラスに「proxy = True」を記述した子クラスで継承する方式。 Models | Django documentation | Django

*9:multi-table inheritance 方式は使わないようにしよう!

*10:ヘルパー関数、フォーム、モデルのメソッドなど、ビュー(やビューに直接関連した部品)以外では使わないこと

*11:https://docs.djangoproject.com/en/1.8/ref/models/database-functions/

*12:書くなら raw() で。extra() は使わない

*13:リクエストごとにしたいのであれば、settings.py の「ATOMIC_REQUESTS」の設定を True にする。ビュー中の特定の範囲にのみトランザクションを適用する場合は「with transaction.non_atomic_requests()」で囲む

*14:複雑にカスタマイズしたい場合だけ関数ベースのビューを使えばよい

*15:Mixin を自作するときは Python object 型を継承し、複雑に継承させない設計にする

*16:View, RedirectView, TemplateView, ListView, DetailView, FormView, CreateView, UpdateView, DeleteView, Generic date views が紹介されている

*17:https://django-braces.readthedocs.io/en/latest/form.html#userformkwargsmixin:UserFormKwargsMixin, https://django-braces.readthedocs.io/en/latest/form.html#userkwargmodelformmixin:UserKwargModelFormMixin など

*18:django-floppyforms, django-crispy-forms, django-forms-bootstrap が紹介されている

*19:select_related の詳細については、過去記事「Django ORM の select_related, prefetch_related の挙動を詳しく調べてみた - akiyoko blog」を参照

*20:パフォーマンスの問題がある場合以外は DTL のままでよい

*21:Python 2.7 の場合は python_2_unicode_compatible デコレータを使うか、__unicode__() メソッドを用意する

*22:AbstractUser を継承する方法、AbstractBaseUser を継承する方法、ユーザモデルに対して一対多・一対一・多対多関連のフィールドを作成する方法

*23:Python, Django 界隈ではあまり使われないが、Markdown 形式も人気。なお、Pandoc を使えばマークアップを自由に変換可能

*24:Djangoフォームで autocomplete を off にする、django.forms.PasswordInput を使うなど

*25:ただし、django.utils 内のモジュールは内部向けに作られたものでバージョンによって挙動が変わるので注意。詳しくは Django Utils | Django documentation | Django を参照

*26:django.core.exceptions のいくつかのビルトイン例外クラスが利用可能

*27:シリアライズ/デシリアライズには django.core.serializers が使える

Mezzanine に Cartridge 0.12 を導入してみる

Django ECサイト Mezzanine

この前 4.2.2 にアップデートした Mezzanine サイトに、Cartridge 0.12 を導入してみました。


Mezzanine は、Python製の WordPress風フルスタックCMSフレームワークですが、一方の Cartridge は、Mezzanine 専用に作られた、Mezzanine に ECサイト機能を搭載するためのアプリケーションです。 *1


イメージとしてはこんな感じです。

f:id:akiyoko:20161120180920p:plain


具体的には、商品登録やセール設定、ディスカウント設定、注文管理などの機能を備えたバックオフィス(Django Admin を拡張)、ショッピングカート、PayPal および Stripe に対応した決済モジュール(プラガブルに変更可能)などの機能が付きます。


f:id:akiyoko:20161120182828p:plain
(ショッピングカート機能)


f:id:akiyoko:20161120184415p:plain
(バックオフィス機能)


 

1. 導入手順

Mezzanine に Cartridge 0.12 を導入する手順は以下の通りです。


Cartridge 0.12.0 をインストールします。

(akiyokoproject)$ pip install cartridge==0.12.0

Successfully installed cartridge-0.12.0 pyPdf2-1.26.0 reportlab-3.3.0 xhtml2pdf-0.0.6


config/settings.py に Cartridge 用の設定を追記します。

--- a/config/settings.py
+++ b/config/settings.py
@@ -7,6 +7,76 @@ from django.utils.translation import ugettext_lazy as _


 ######################
+# CARTRIDGE SETTINGS #
+######################
+
+# The following settings are already defined in cartridge.shop.defaults
+# with default values, but are common enough to be put here, commented
+# out, for conveniently overriding. Please consult the settings
+# documentation for a full list of settings Cartridge implements:
+# http://cartridge.jupo.org/configuration.html#default-settings
+
+# Sequence of available credit card types for payment.
+# SHOP_CARD_TYPES = ("Mastercard", "Visa", "Diners", "Amex")
+
+# Setting to turn on featured images for shop categories. Defaults to False.
+# SHOP_CATEGORY_USE_FEATURED_IMAGE = True
+
+# Set an alternative OrderForm class for the checkout process.
+# SHOP_CHECKOUT_FORM_CLASS = 'cartridge.shop.forms.OrderForm'
+
+# If True, the checkout process is split into separate
+# billing/shipping and payment steps.
+# SHOP_CHECKOUT_STEPS_SPLIT = True
+
+# If True, the checkout process has a final confirmation step before
+# completion.
+# SHOP_CHECKOUT_STEPS_CONFIRMATION = True
+
+# Controls the formatting of monetary values accord to the locale
+# module in the python standard library. If an empty string is
+# used, will fall back to the system's locale.
+# SHOP_CURRENCY_LOCALE = ""
+
+# Dotted package path and name of the function that
+# is called on submit of the billing/shipping checkout step. This
+# is where shipping calculation can be performed and set using the
+# function ``cartridge.shop.utils.set_shipping``.
+# SHOP_HANDLER_BILLING_SHIPPING = \
+#                       "cartridge.shop.checkout.default_billship_handler"
+
+# Dotted package path and name of the function that
+# is called once an order is successful and all of the order
+# object's data has been created. This is where any custom order
+# processing should be implemented.
+# SHOP_HANDLER_ORDER = "cartridge.shop.checkout.default_order_handler"
+
+# Dotted package path and name of the function that
+# is called on submit of the payment checkout step. This is where
+# integration with a payment gateway should be implemented.
+# SHOP_HANDLER_PAYMENT = "cartridge.shop.checkout.default_payment_handler"
+
+# Sequence of value/name pairs for order statuses.
+# SHOP_ORDER_STATUS_CHOICES = (
+#     (1, "Unprocessed"),
+#     (2, "Processed"),
+# )
+
+# Sequence of value/name pairs for types of product options,
+# eg Size, Colour. NOTE: Increasing the number of these will
+# require database migrations!
+# SHOP_OPTION_TYPE_CHOICES = (
+#     (1, "Size"),
+#     (2, "Colour"),
+# )
+
+# Sequence of indexes from the SHOP_OPTION_TYPE_CHOICES setting that
+# control how the options should be ordered in the admin,
+# eg for "Colour" then "Size" given the above:
+# SHOP_OPTION_ADMIN_ORDER = (2, 1)
+
+
+######################
 # MEZZANINE SETTINGS #
 ######################

@@ -21,7 +91,9 @@ from django.utils.translation import ugettext_lazy as _
 #
 # ADMIN_MENU_ORDER = (
 #     ("Content", ("pages.Page", "blog.BlogPost",
-#        "generic.ThreadedComment", (_("Media Library"), "media-library"),)),
+#        "generic.ThreadedComment", (_("Media Library"), "fb_browse"),)),
+#     (_("Shop"), ("shop.Product", "shop.ProductOption", "shop.DiscountCode",
+#        "shop.Sale", "shop.Order")),
 #     ("Site", ("sites.Site", "redirects.Redirect", "conf.Setting")),
 #     ("Users", ("auth.User", "auth.Group",)),
 # )
@@ -247,6 +319,7 @@ INSTALLED_APPS = (
     "mezzanine.core",
     "mezzanine.generic",
     "mezzanine.pages",
+    "cartridge.shop",
     "mezzanine.blog",
     "mezzanine.forms",
     "mezzanine.galleries",
@@ -272,6 +345,7 @@ MIDDLEWARE_CLASSES = (
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',

+    "cartridge.shop.middleware.ShopMiddleware",
     "mezzanine.core.request.CurrentRequestMiddleware",
     "mezzanine.core.middleware.RedirectFallbackMiddleware",
     "mezzanine.core.middleware.TemplateForDeviceMiddleware",
diff --git a/config/urls.py b/config/urls.py
index c67fd17..1b3b87c 100644
--- a/config/urls.py
+++ b/config/urls.py
@@ -8,6 +8,8 @@ from django.views.i18n import set_language
 from mezzanine.core.views import direct_to_template
 from mezzanine.conf import settings

+from cartridge.shop.views import order_history
+

 admin.autodiscover()

@@ -27,6 +29,10 @@ if settings.USE_MODELTRANSLATION:
     ]

 urlpatterns += [
+    # Cartridge URLs.
+    url("^shop/", include("cartridge.shop.urls")),
+    url("^account/orders/$", order_history, name="shop_order_history"),
+
     # We don't want to presume how your homepage works, so here are a
     # few patterns you can use to set it up.

@@ -93,7 +99,7 @@ urlpatterns += [
     # Note that for any of the various homepage patterns above, you'll
     # need to use the ``SITE_PREFIX`` setting as well.

-    # ("^%s/" % settings.SITE_PREFIX, include("mezzanine.urls"))
+    # url("^%s/" % settings.SITE_PREFIX, include("mezzanine.urls"))

 ]

diff --git a/requirements.txt b/requirements.txt
index 0c4e494..4d43fbe 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 Mezzanine==4.2.2
+cartridge==0.12.0
 django-ses

追加した内容については、Mezzanine と Cartridge を同時にインストールするコマンドを叩いたときの settings.py の差分を参照しました(詳しくは後述)。


マイグレーションを実行します。

(akiyokoproject)$ python manage.py makemigrations
No changes detected

(akiyokoproject)$ python manage.py migrate
System check identified some issues:

WARNINGS:
?: (mysql.W002) MySQL Strict Mode is not set for database connection 'default'
	HINT: MySQL's Strict Mode fixes many data integrity problems in MySQL, such as data truncation upon insertion, by escalating warnings into errors. It is strongly recommended you activate it. See: https://docs.djangoproject.com/en/1.10/ref/databases/#mysql-sql-mode
Operations to perform:
  Apply all migrations: admin, auth, blog, conf, contenttypes, core, django_comments, forms, galleries, generic, pages, redirects, sessions, shop, sites, twitter
Running migrations:
  Applying shop.0001_initial... OK
  Applying shop.0002_auto_20141227_1331... OK
  Applying shop.0003_emailfield... OK
  Applying shop.0004_productimage_file_field... OK
  Applying shop.0005_auto_20150527_1127... OK
  Applying shop.0006_auto_20150916_0459... OK
  Applying shop.0007_auto_20150921_2323... OK

shop 関連のテーブルが作成されました。




以下の Cartridge のテンプレートを、templates/ および custom/ 配下にコピーします。
(例によって、admin 関連のテンプレートは修正しないため、コピー対象に含めていません。)

templates/accounts/account_profile_update.html
templates/email/base.html
templates/email/order_receipt.html
templates/email/order_receipt.txt
templates/email/receipt.html
templates/email/receipt_rtl.html
templates/pages/category.html
templates/shop/base.html
templates/shop/billing_shipping.html
templates/shop/cart.html
templates/shop/checkout.html
templates/shop/complete.html
templates/shop/confirmation.html
templates/shop/includes/order_details.html
templates/shop/includes/order_details_rtl.html
templates/shop/includes/order_totals.html
templates/shop/includes/order_totals.txt
templates/shop/includes/payment_fields.html
templates/shop/includes/user_panel.html
templates/shop/order_history.html
templates/shop/order_invoice.html
templates/shop/order_invoice_pdf.html
templates/shop/payment.html
templates/shop/product.html
templates/shop/wishlist.html

custom/templates/accounts/account_profile_update.html
custom/templates/email/base.html
custom/templates/email/order_receipt.html
custom/templates/email/order_receipt.txt
custom/templates/email/receipt.html
custom/templates/email/receipt_rtl.html
custom/templates/pages/category.html
custom/templates/shop/base.html
custom/templates/shop/billing_shipping.html
custom/templates/shop/cart.html
custom/templates/shop/checkout.html
custom/templates/shop/complete.html
custom/templates/shop/confirmation.html
custom/templates/shop/includes/order_details.html
custom/templates/shop/includes/order_details_rtl.html
custom/templates/shop/includes/order_totals.html
custom/templates/shop/includes/order_totals.txt
custom/templates/shop/includes/payment_fields.html
custom/templates/shop/includes/user_panel.html
custom/templates/shop/order_history.html
custom/templates/shop/order_invoice.html
custom/templates/shop/order_invoice_pdf.html
custom/templates/shop/payment.html
custom/templates/shop/product.html
custom/templates/shop/wishlist.html


最後に、プロセスを再起動して完了です。

(akiyokoproject)$ sudo supervisorctl restart all

 

細かい話

2.1. ライブラリの差分

Cartridge 導入前のライブラリ一覧

awscli (1.11.10)
beautifulsoup4 (4.5.1)
bleach (1.4.3)
boto (2.41.0)
botocore (1.4.67)
chardet (2.3.0)
colorama (0.3.7)
Django (1.10.3)
django-appconf (1.0.2)
django-compressor (2.0)
django-contrib-comments (1.7.3)
django-ses (0.8.0)
docutils (0.12)
filebrowser-safe (0.4.6)
future (0.16.0)
futures (3.0.5)
grappelli-safe (0.4.5)
gunicorn (19.6.0)
html5lib (0.9999999)
jmespath (0.9.0)
Mezzanine (4.2.2)
MySQL-python (1.2.5)
oauthlib (2.0.0)
Pillow (3.4.2)
pip (8.1.2)
psycopg2 (2.6.2)
pyasn1 (0.1.9)
python-dateutil (2.5.3)
python-memcached (1.58)
pytz (2016.7)
rcssmin (1.0.6)
requests (2.11.1)
requests-oauthlib (0.7.0)
rjsmin (1.0.12)
rsa (3.4.2)
s3transfer (0.1.9)
setproctitle (1.1.10)
setuptools (24.0.2)
six (1.10.0)
tzlocal (1.3)
wheel (0.29.0)

導入後のライブラリ一覧

$ pip list
awscli (1.11.10)
beautifulsoup4 (4.5.1)
bleach (1.4.3)
boto (2.41.0)
botocore (1.4.67)
Cartridge (0.12.0)
chardet (2.3.0)
colorama (0.3.7)
Django (1.10.3)
django-appconf (1.0.2)
django-compressor (2.0)
django-contrib-comments (1.7.3)
django-ses (0.8.0)
docutils (0.12)
filebrowser-safe (0.4.6)
future (0.16.0)
futures (3.0.5)
grappelli-safe (0.4.5)
gunicorn (19.6.0)
html5lib (0.9999999)
jmespath (0.9.0)
Mezzanine (4.2.2)
MySQL-python (1.2.5)
oauthlib (2.0.0)
Pillow (3.4.2)
pip (8.1.2)
psycopg2 (2.6.2)
pyasn1 (0.1.9)
PyPDF2 (1.26.0)
python-dateutil (2.5.3)
python-memcached (1.58)
pytz (2016.7)
rcssmin (1.0.6)
reportlab (3.3.0)
requests (2.11.1)
requests-oauthlib (0.7.0)
rjsmin (1.0.12)
rsa (3.4.2)
s3transfer (0.1.9)
setproctitle (1.1.10)
setuptools (24.0.2)
six (1.10.0)
tzlocal (1.3)
wheel (0.29.0)
xhtml2pdf (0.0.6)


 

2.2. config/setting.py の差分

Mezzanine と Cartridge を同時にインストールするには、

mezzanine-project -a cartridge config .

というコマンドを叩けばよいのですが、Mezzanine だけをインストールした場合と Mezzanine と Cartridge を同時にインストールした場合とで、自動生成される settings.py の差分を確認してみました。


1)(Cartridge 無しの)通常 Mezzanine インストール
sudo mkdir -p /opt/webapps/mezzproject
sudo chown -R `whoami`. /opt/webapps
cd /opt/webapps/mezzproject/
mezzanine-project config .
2)Cartridge 付きの Mezzanine インストール
sudo mkdir -p /opt/webapps/mezzproject_cartridge
sudo chown -R `whoami`. /opt/webapps
cd /opt/webapps/mezzproject_cartridge/
mezzanine-project -a cartridge config .


(diff 関連の参考リンク)

$ diff -r -q /opt/webapps/mezzproject /opt/webapps/mezzproject_cartridge
$ diff -r -u /opt/webapps/mezzproject /opt/webapps/mezzproject_cartridge | vi -R -

-q : ファイル名だけ表示
-r : ディレクトリ比較
-u : ユニファイド形式
| vi -R - : vim の機能を使って色分け

ちなみに、進んでいる方を第二引数にした方がよさそうです(+ で diff表示されるので)。

$ diff -r -u /opt/webapps/mezzproject /opt/webapps/mezzproject_cartridge
Only in /opt/webapps/mezzproject_cartridge/config: dev.db
diff -r -u /opt/webapps/mezzproject/config/local_settings.py /opt/webapps/mezzproject_cartridge/config/local_settings.py
--- /opt/webapps/mezzproject/config/local_settings.py   2016-11-03 03:06:58.160419703 +0000
+++ /opt/webapps/mezzproject_cartridge/config/local_settings.py 2016-11-03 03:07:09.000000000 +0000
@@ -8,8 +8,8 @@
 DEBUG = True

 # Make these unique, and don't share it with anybody.
-SECRET_KEY = "k8dd+%#@kw6vh1a-#k(l1agb=@kibpbz&s$7nk)l@s06gz*gx9"
-NEVERCACHE_KEY = "15fmx+kop%oxps(1w3uq5i1btc#3t=+t!#=ax@3!4^@$s%4gu2"
+SECRET_KEY = "(#eotia6m9*m(9vt)@u7^6@)e$f44c(4)4n9*((o%(^u3!u&cw"
+NEVERCACHE_KEY = "n66suh9!=@^-_6^toe)s_z)c$0g70e%in1ifzo7w%&uf&b#z(y"

 DATABASES = {
     "default": {
diff -r -u /opt/webapps/mezzproject/config/settings.py /opt/webapps/mezzproject_cartridge/config/settings.py
--- /opt/webapps/mezzproject/config/settings.py 2016-11-03 03:06:58.164419703 +0000
+++ /opt/webapps/mezzproject_cartridge/config/settings.py   2016-11-03 03:07:09.000000000 +0000
@@ -7,6 +7,76 @@


 ######################
+# CARTRIDGE SETTINGS #
+######################
+
+# The following settings are already defined in cartridge.shop.defaults
+# with default values, but are common enough to be put here, commented
+# out, for conveniently overriding. Please consult the settings
+# documentation for a full list of settings Cartridge implements:
+# http://cartridge.jupo.org/configuration.html#default-settings
+
+# Sequence of available credit card types for payment.
+# SHOP_CARD_TYPES = ("Mastercard", "Visa", "Diners", "Amex")
+
+# Setting to turn on featured images for shop categories. Defaults to False.
+# SHOP_CATEGORY_USE_FEATURED_IMAGE = True
+
+# Set an alternative OrderForm class for the checkout process.
+# SHOP_CHECKOUT_FORM_CLASS = 'cartridge.shop.forms.OrderForm'
+
+# If True, the checkout process is split into separate
+# billing/shipping and payment steps.
+# SHOP_CHECKOUT_STEPS_SPLIT = True
+
+# If True, the checkout process has a final confirmation step before
+# completion.
+# SHOP_CHECKOUT_STEPS_CONFIRMATION = True
+
+# Controls the formatting of monetary values accord to the locale
+# module in the python standard library. If an empty string is
+# used, will fall back to the system's locale.
+# SHOP_CURRENCY_LOCALE = ""
+
+# Dotted package path and name of the function that
+# is called on submit of the billing/shipping checkout step. This
+# is where shipping calculation can be performed and set using the
+# function ``cartridge.shop.utils.set_shipping``.
+# SHOP_HANDLER_BILLING_SHIPPING = \
+#                       "cartridge.shop.checkout.default_billship_handler"
+
+# Dotted package path and name of the function that
+# is called once an order is successful and all of the order
+# object's data has been created. This is where any custom order
+# processing should be implemented.
+# SHOP_HANDLER_ORDER = "cartridge.shop.checkout.default_order_handler"
+
+# Dotted package path and name of the function that
+# is called on submit of the payment checkout step. This is where
+# integration with a payment gateway should be implemented.
+# SHOP_HANDLER_PAYMENT = "cartridge.shop.checkout.default_payment_handler"
+
+# Sequence of value/name pairs for order statuses.
+# SHOP_ORDER_STATUS_CHOICES = (
+#     (1, "Unprocessed"),
+#     (2, "Processed"),
+# )
+
+# Sequence of value/name pairs for types of product options,
+# eg Size, Colour. NOTE: Increasing the number of these will
+# require database migrations!
+# SHOP_OPTION_TYPE_CHOICES = (
+#     (1, "Size"),
+#     (2, "Colour"),
+# )
+
+# Sequence of indexes from the SHOP_OPTION_TYPE_CHOICES setting that
+# control how the options should be ordered in the admin,
+# eg for "Colour" then "Size" given the above:
+# SHOP_OPTION_ADMIN_ORDER = (2, 1)
+
+
+######################
 # MEZZANINE SETTINGS #
 ######################

@@ -21,7 +91,9 @@
 #
 # ADMIN_MENU_ORDER = (
 #     ("Content", ("pages.Page", "blog.BlogPost",
-#        "generic.ThreadedComment", (_("Media Library"), "media-library"),)),
+#        "generic.ThreadedComment", (_("Media Library"), "fb_browse"),)),
+#     (_("Shop"), ("shop.Product", "shop.ProductOption", "shop.DiscountCode",
+#        "shop.Sale", "shop.Order")),
 #     ("Site", ("sites.Site", "redirects.Redirect", "conf.Setting")),
 #     ("Users", ("auth.User", "auth.Group",)),
 # )
@@ -243,6 +315,7 @@
     "mezzanine.core",
     "mezzanine.generic",
     "mezzanine.pages",
+    "cartridge.shop",
     "mezzanine.blog",
     "mezzanine.forms",
     "mezzanine.galleries",
@@ -251,6 +324,7 @@
     # "mezzanine.mobile",
 )

+
 # List of middleware classes to use. Order is important; in the request phase,
 # these middleware classes will be applied in the order given, and in the
 # response phase the middleware will be applied in reverse order.
@@ -267,12 +341,15 @@
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',

+    "cartridge.shop.middleware.ShopMiddleware",
     "mezzanine.core.request.CurrentRequestMiddleware",
     "mezzanine.core.middleware.RedirectFallbackMiddleware",
     "mezzanine.core.middleware.TemplateForDeviceMiddleware",
     "mezzanine.core.middleware.TemplateForHostMiddleware",
     "mezzanine.core.middleware.AdminLoginInterfaceSelectorMiddleware",
     "mezzanine.core.middleware.SitePermissionMiddleware",
+    # Uncomment the following if using any of the SSL settings:
+    # "mezzanine.core.middleware.SSLRedirectMiddleware",
     "mezzanine.pages.middleware.PageMiddleware",
     "mezzanine.core.middleware.FetchFromCacheMiddleware",
 )
diff -r -u /opt/webapps/mezzproject/config/urls.py /opt/webapps/mezzproject_cartridge/config/urls.py
--- /opt/webapps/mezzproject/config/urls.py 2016-11-03 03:06:58.160419703 +0000
+++ /opt/webapps/mezzproject_cartridge/config/urls.py   2016-11-03 03:07:09.000000000 +0000
@@ -4,10 +4,11 @@
 from django.conf.urls.i18n import i18n_patterns
 from django.contrib import admin
 from django.views.i18n import set_language
-
 from mezzanine.core.views import direct_to_template
 from mezzanine.conf import settings

+from cartridge.shop.views import order_history
+

 admin.autodiscover()

@@ -27,6 +28,11 @@
     ]

 urlpatterns += [
+
+    # Cartridge URLs.
+    url("^shop/", include("cartridge.shop.urls")),
+    url("^account/orders/$", order_history, name="shop_order_history"),
+
     # We don't want to presume how your homepage works, so here are a
     # few patterns you can use to set it up.

@@ -93,7 +99,7 @@
     # Note that for any of the various homepage patterns above, you'll
     # need to use the ``SITE_PREFIX`` setting as well.

-    # ("^%s/" % settings.SITE_PREFIX, include("mezzanine.urls"))
+    # url("^%s/" % settings.SITE_PREFIX, include("mezzanine.urls"))

 ]

Only in /opt/webapps/mezzproject_cartridge: .DS_Store


以上を踏まえて、内容を追加しました。

*1:Mezzanine 公式ページには「Ecommerce / Shopping cart module」と紹介されています。

IPA「情報セキュリティマネジメント試験」に一夜漬けで合格するためのたった二つの勉強法

セキュリティ 資格

f:id:akiyoko:20161117002302j:plain

こんにちは、akiyoko です。


先月の IPA(情報処理推進機構)の秋試験で、「情報セキュリティマネジメント試験」に 一夜漬けで合格 することができました!!


パチパチパチ!


先日合格発表があり、めでたく合格できたことを確認しました。

f:id:akiyoko:20161115005143p:plain


試験対策としては、試験前日に数時間やっただけです。
成績照会の結果を見てみると、午前が90点・午後が88点だったので(合格基準は午前・午後それぞれ 60点)余裕の合格と言ってもいいでしょう。



そもそも、「情報セキュリティマネジメント試験」(略号「SG」)は、2016年(平成28年)の春試験からスタートした新しい国家試験(情報処理技術者試験)です。


高度化する IT、多様化・複雑化するサイバー攻撃や脅威に対応できる情報セキュリティマネジメントを担う人材がますます求められているという時代背景に合わせて創設された、いわば「今必要な人材のための新しい資格」とも言えます。


共通キャリア・スキルフレームワーク(CCSF)レベル 2相当なので、他の試験で言うと「基本情報技術者試験」(FE)と同程度のレベルということになります。

f:id:akiyoko:20161115010733p:plain
(試験区分 「IPA 独立行政法人 情報処理推進機構:試験要綱 Ver3.0」より) *1


難易度としてはぶっちゃけ、IT業界で長く働いていればそんなに難しくない試験だと思いますが、セキュリティ関連の専門用語・専門知識が必要になるので、それらの分野が苦手な人はそれなりに勉強が必要になってきます。

私見としては、受験資格(条件)は特に無く *2、年に二回(4月と10月)実施されるため受験チャンスも多く、文章記述の問題が無く(全てマークシート方式)、試験時間も比較的短く(午前と午後で90分ずつ)、受験料もリーズナブル(5,700円)で、気軽に受験できる国家資格だと思います。

時間区分 午前 午後
試験時間 90分 90分
出題形式 多肢選択式(四肢択一) 多肢選択式
合格基準 60点(100点満点) 60点(100点満点)


こちらの公式ページから、サンプル問題(午前・午後)と平成28年度春期試験の問題、平成28年度春期試験の問題をチェックすることができますので、気になる方は一度チラ見してみてください。




さて、この「情報セキュリティマネジメント試験」の受験を勧める対象者としては、

  • 業務で個人情報を取り扱う全ての方
  • 業務部門・管理部門で情報管理を担当する全ての方
  • 外部委託先に対する情報セキュリティ評価・確認を行う全ての方
  • 情報セキュリティ管理の知識・スキルを身に付けたい全ての方
  • パス(ITパスポート試験)合格から、さらにステップアップしたい全ての方

と書いてありますが(*3)、実際に受験会場でざっと見回した限りでは、40代〜50代の管理職っぽい面々もちらほら見受けられました。予想以上に平均年齢が高い印象です。会社から「セキュリティの資格を取れ」と言われていたけど「情報処理安全確保支援士試験」(旧セキュリティスペシャリスト試験)までは手が出せなかった層にある程度のニーズがあるのかもしれません。





 

勉強法①:午前対策は「過去問よりも参考書」

私はその他の資格試験では一貫して「過去問絶対主義」だったのですが、この試験だけは勝手が違うようです。


何と、他の IPA試験だったらそのままの問題が何問か出るはずの過去問から、一問も出なかったのです。

その代わり、私のチェックした限りで、基本情報技術者試験の午前問題から4問、応用情報技術者試験の午前問題から2問、全く同じ問題が出題されていました。


今回買った参考書はこの本なのですが、振り返ってみると大当たりでした。まるっきり同じ過去問が出題されたその6問が練習問題として含まれていたので、この本一冊と心中してしまってもよいでしょう。

情報処理教科書 情報セキュリティマネジメント 2016年秋期

情報処理教科書 情報セキュリティマネジメント 2016年秋期



もちろん、全く同じ問題ではなくても似たような問題(例えば「rootkit」という用語を知っていれば答えられる、等)がいくつか出題されていたので、もちろん過去問もしっかりやっておくべきでしょう。


ということで午前対策は、過去問をひと通り、プラス参考書をざっと通し読みするだけ で大丈夫です。合計で 3〜4時間くらいでしょうか。




なお、参考書を Kindle で買うのか、紙の本で買うのかという議論がありますが、正直なところ、一長一短かと思います。今回は、試験対策本として初めて Kindle 版を買ってみたのですが、私はどちらかと言うと紙の本の方が合ってるかなぁという感じです。


<メリット>

  • 持ち運びに便利
  • ちょっとだけ安い
  • 通勤電車で読んでいても恥ずかしくない(!)
  • 直前に読んでいても「必死に勉強している感」を隠せる(!!)

<デメリット>

  • ページを行ったり来たりするのが面倒(特に、過去問の問題と解答・解説)


 

勉強法②:午後対策は「図を描きながらストーリーを理解」

午後は、6〜8ページほどの長文を読んで多肢選択式の設問に答えるタイプの問題が 3問も待ち構えています。


長文を読むのは結構つらいです。文章が長いので、読み終わる頃には前半部分の登場人物や所属なんかを忘れてしまっていることも。。

そこで、多少時間は掛かってしまいますが、下のような組織構造を表した図を描いて全体像をメモしながら状況を把握するのがよいでしょう。

f:id:akiyoko:20161116021359p:plain

コツとしては、問題が違ってもだいたい似たような構成になるはず(情報システム部があって情報セキュリティ責任者とリーダーがいる、等)なので、パターンに慣れることが重要です。


また、問題文にはお決まりの「起承転結」があるので、ストーリーの流れを図の状況と合わせて理解していきましょう。


例えば、こんな感じで問題文が書かれているはずです。

《 起 》 企業や組織の業務、および情報システム部についての紹介
《 承 》 事前のセキュリティ対策や企業や組織個別の事情など
《 転 》 セキュリティ事故発生!!
《 結 》 セキュリティ責任者による事態の収束とセキュリティ対策の評価・改善


どんなセキュリティ事故が起きても、必ず最後はハッピーエンドで終わります。


午後対策は、「組織図」と「ストーリーの流れ」に注目しながら過去問一回分を解けば十分 でしょう。



 

まとめ

午前対策としては、上で紹介した「情報処理教科書 情報セキュリティマネジメント 2016年秋期」を買って、数時間でざっとひと通り読み込んでおきましょう。頻出するセキュリティ用語・知識を網羅しておくことがキーポイントになりますが、この「情報セキュリティマネジメント試験」では他試験(基本情報技術者試験および応用情報技術者試験)の過去問がそのまま出ることが多いので、分からない問題はいっそのこと丸暗記してしまってもよいでしょう(あくまで一夜漬けとして)。


午後対策としては、「図を描きながらストーリーを理解」することを挙げました。問題文は長いですが、ストーリーの流れや組織図はパターン化されていますので、過去問で少し慣れてしまえば、あとは恐れるに足りません。


これであなたも来春には「情報セキュリティマネジメント試験」合格ですね!!

*1:2017年の春試験から、「情報セキュリティスペシャリスト試験」が「情報処理安全確保支援士試験」となり、登録制になるのがこれまでと大きく異なります。

*2:年齢や国籍等を含め受験する上での制限はありません。また、どの試験区分からでも受験いただけます。(「IPA 独立行政法人 情報処理推進機構:情報処理技術者試験:よくある質問」より)

*3:情報セキュリティマネジメント試験|平成28年度春期開始 より

「D3.jsで学ぶデータビジュアライゼーション」に参加してきました

勉強会 D3.js 統計解析

会場

Twitter Japan
東京都中央区京橋3−1−1 東京スクエアガーデン19階


(参考)Twitter Japan に行ってきた! - 941::blog



Twitter

twitter.com


 

全体の感想など

今年の 6/28 に D3.js がバージョン 4系にメジャーバージョンアップしたとのことですが、バージョン 3 系がリリースされたのが 2012年12月だったので、およそ 3年半ぶりのメジャーバージョンアップとなったようです。そういえば、二年ほど前に個人的にいろいろ試していたバージョンも 3.3 or 3.4 でした。だいぶ息の長いバージョンになりましたね。サンプルやドキュメント、プラグインがまだ追い付いていないらしいので、現場ではまだしばらくはバージョン 3系が使われそうです。


<過去記事>
akiyoko.hatenablog.jp


後半の発表については、Python と D3.js を連携させるときのノウハウ(REST API を使うとか)だったり、どういうふうに役割分担させたか(例えば、データ検索と前処理は Python / NumPy とか)だったり、バックエンドに Python を使うことの明確なメリット(データ件数とレスポンスタイムの比較)だったりを期待していたので、少し思惑とは違った内容でした。


もちろん個人的にいくつか得るものがありましたし、参加者も多くて全体的な雰囲気も活気があったように見受けられ、データビジュアライゼーションはまだまだ人気分野だと感じました。

なお、今回は有料イベント(1,000円)でした。




 

「細かすぎて伝わらないD3 ver.4の話」

清水 正行氏

(参考)





 

「PythonとD3.jsで作るデータベースの活用を広げる可視化アプリケーション開発」

オーイシ ナオヤ氏


  • 生命科学関連データベースの利用促進のための Webサービス開発
  • AOE: http://aoe.dbcls.jp/
    • 遺伝子発現データベースの可視化サービス
  • 使っているもの
  • Tableau, Spotfire じゃダメなの?? 何で Webサービス?
  • SPARQL Query Filter
  • データが大きい場合や、独自のアルゴリズムで統計処理したものを可視化したい場合は、Python で事前に処理して D3.js に渡した方が軽快かも


 

参考本

サンプルが多くて初心者向け。

インタラクティブ・データビジュアライゼーション ―D3.jsによるデータの可視化

インタラクティブ・データビジュアライゼーション ―D3.jsによるデータの可視化

現場ですぐ使える時系列データ分析 ~データサイエンティストのための基礎知識~

現場ですぐ使える時系列データ分析 ~データサイエンティストのための基礎知識~

本番運用しているブログサイトの Mezzanine を 4.1.0 から 4.2.2 にアップデートしてみた

Mezzanine 運用 Django

Monotalk さんの以下の記事に触発されて、私が本番運用している某ブログサイトの Mezzanine を 4.1.0 から 4.2.2 にアップデートしてみました。


アップデートするのに結構苦労したように書いてあったので、念のため、Vagrant の開発環境と EC2 のバックアップイメージから起動したインスタンス上でのリハーサルをしてから臨みました。


結論から言うと、あまり苦労せずにアップデートできました。Monotalk さんは Mezzanine 3 系から運用していたような感じだったので、そこからの負債が溜まっていたのではないかと推測します。


 

アップデート手順

アップデートの手順はたったこれだけです。

$ pip install -U Mezzanine==4.2.2

$ python manage.py makemigrations
$ python manage.py migrate



アップデート前の本番環境のインストール済みライブラリ一覧(2016/11/2 時点)

$ pip list
awscli (1.11.10)
beautifulsoup4 (4.5.1)
bleach (1.4.3)
boto (2.41.0)
botocore (1.4.67)
chardet (2.3.0)
colorama (0.3.7)
Django (1.9.9)
django-appconf (1.0.2)
django-compressor (2.0)
django-contrib-comments (1.7.2)
django-ses (0.8.0)
docutils (0.12)
filebrowser-safe (0.4.5)
future (0.15.2)
futures (3.0.5)
grappelli-safe (0.4.4)
gunicorn (19.6.0)
html5lib (0.9999999)
jmespath (0.9.0)
Mezzanine (4.1.0)
MySQL-python (1.2.5)
oauthlib (1.1.2)
Pillow (3.3.1)
pip (8.1.2)
psycopg2 (2.6.2)
pyasn1 (0.1.9)
python-dateutil (2.5.3)
python-memcached (1.58)
pytz (2016.6.1)
rcssmin (1.0.6)
requests (2.11.1)
requests-oauthlib (0.6.2)
rjsmin (1.0.12)
rsa (3.4.2)
s3transfer (0.1.9)
setproctitle (1.1.10)
setuptools (24.0.2)
six (1.10.0)
tzlocal (1.2.2)
wheel (0.29.0)


アップデート後のインストール済みライブラリの一覧は、以下のようになりました。

$ pip list
awscli (1.11.10)
beautifulsoup4 (4.5.1)
bleach (1.4.3)
boto (2.41.0)
botocore (1.4.67)
chardet (2.3.0)
colorama (0.3.7)
Django (1.10.3)
django-appconf (1.0.2)
django-compressor (2.0)
django-contrib-comments (1.7.3)
django-ses (0.8.0)
docutils (0.12)
filebrowser-safe (0.4.6)
future (0.16.0)
futures (3.0.5)
grappelli-safe (0.4.5)
gunicorn (19.6.0)
html5lib (0.9999999)
jmespath (0.9.0)
Mezzanine (4.2.2)
MySQL-python (1.2.5)
oauthlib (2.0.0)
Pillow (3.4.2)
pip (8.1.2)
psycopg2 (2.6.2)
pyasn1 (0.1.9)
python-dateutil (2.5.3)
python-memcached (1.58)
pytz (2016.7)
rcssmin (1.0.6)
requests (2.11.1)
requests-oauthlib (0.7.0)
rjsmin (1.0.12)
rsa (3.4.2)
s3transfer (0.1.9)
setproctitle (1.1.10)
setuptools (24.0.2)
six (1.10.0)
tzlocal (1.3)
wheel (0.29.0)


Mezzanine 4.1.0 から 4.2.2 への一番の変更点は、Django のバージョンが 1.9 系から 1.10 系にアップデートされることでしょうか。

4.1.0 から 4.2.2 への全ての変更箇所は以下で確認することができます。
https://github.com/stephenmcd/mezzanine/compare/4.1.0...4.2.2


このアップデートでいくつかのバグ(*1)が本家で修正されたのに伴い、技術的な負債を少し解消することができました。



 

影響

4.2.2 へのアップデート後、ping_google コマンド実行時にエラーが出るようになってしまいました。

$ python manage.py ping_google
Traceback (most recent call last):
  File "manage.py", line 14, in <module>
    execute_from_command_line(sys.argv)
  File "/home/vagrant/.virtualenvs/akiyokoproject/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 367, in execute_from_command_line
    utility.execute()
  File "/home/vagrant/.virtualenvs/akiyokoproject/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 359, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/home/vagrant/.virtualenvs/akiyokoproject/local/lib/python2.7/site-packages/django/core/management/base.py", line 294, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/home/vagrant/.virtualenvs/akiyokoproject/local/lib/python2.7/site-packages/django/core/management/base.py", line 345, in execute
    output = self.handle(*args, **options)
  File "/home/vagrant/.virtualenvs/akiyokoproject/local/lib/python2.7/site-packages/django/contrib/sitemaps/management/commands/ping_google.py", line 12, in handle
    ping_google(sitemap_url=options['sitemap_url'])
  File "/home/vagrant/.virtualenvs/akiyokoproject/local/lib/python2.7/site-packages/django/contrib/sitemaps/__init__.py", line 36, in ping_google
    raise SitemapNotFound("You didn't provide a sitemap_url, and the sitemap URL couldn't be auto-detected.")
django.contrib.sitemaps.SitemapNotFound: You didn't provide a sitemap_url, and the sitemap URL couldn't be auto-detected.

いろいろ調査したのですが、まだ解決していません。。

Django 本体の差分にも原因があるのかなぁ??
https://fossies.org/diffs/Django/1.9.8_vs_1.10/django/contrib/sitemaps/__init__.py-diff.html



 

バックアップイメージでの検証

EC2 のバックアップイメージから起動したインスタンス上でのリハーサル手順をメモしておきます。

1. バックアップイメージからインスタンス起動

まずは、EC2 インスタンスの Create Image をおこない、AMI を作成します。

作成したイメージからインスタンスを起動し、新たな Elastic IP を付与します。


 

2. settings.py の修正

config/local_settings.py

ALLOWED_HOSTS = ['akiyoko.com']

を、

ALLOWED_HOSTS = ['52.199.xx.xx']

と修正します(新しい IP アドレスを「52.199.xx.xx」と想定)。


最後に、プロセスを再起動。

$ sudo supervisorctl restart all


なおこれを修正しないと、アクセス時に Bad Request (400) が発生してしまいます。

(参考)django - Bad request 400: nginx / gunicorn - Stack Overflow

 

3. Nginx の設定ファイル修正

server_name に指定しているドメインを、Elastic IP に変更します。
また、テスト検証では HTTPS は使用しないので、SSL の設定は取り除きます。


/etc/nginx/sites-enabled/akiyokoproject.conf

(変更前)

server {

    #listen 80;
     listen 443 ssl;
    server_name akiyoko.com;
    client_max_body_size 10M;
    keepalive_timeout    15;
    error_log /home/webapp/logs/akiyokoproject_error_nginx.log info;

     ssl_certificate      conf/akiyokoproject.crt;
     ssl_certificate_key  conf/akiyokoproject.key;
     ssl_session_cache    shared:SSL:10m;
     ssl_session_timeout  10m;
     ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA;
     ssl_prefer_server_ciphers on;

    ・
    ・

server {
    listen 80;
    server_name akiyoko.com;
    return 301 https://akiyoko.com$request_uri;
}

server {
    listen 80;
    listen 443 ssl;
    server_name www.akiyoko.com;
    return 301 https://akiyoko.com$request_uri;
}


(変更後)

server {

    listen 80;
    # listen 443 ssl;
    server_name 52.199.xx.xx;
    client_max_body_size 10M;
    keepalive_timeout    15;
    error_log /home/webapp/logs/akiyokoproject_error_nginx.log info;

     #ssl_certificate      conf/akiyokoproject.crt;
     #ssl_certificate_key  conf/akiyokoproject.key;
     #ssl_session_cache    shared:SSL:10m;
     #ssl_session_timeout  10m;
     #ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA;
     #ssl_prefer_server_ciphers on;

    ・
    ・

#server {
#    listen 80;
#    server_name akiyoko.com;
#    return 301 https://akiyoko.com$request_uri;
#}

#server {
#    listen 80;
#    listen 443 ssl;
#    server_name www.akiyoko.com;
#    return 301 https://akiyoko.com$request_uri;
#}


最後に Nginx をリロード。

$ sudo service nginx reload


 

4. Mezzanine アップデート

$ workon akiyokoproject
(akiyokoproject)$ pip install -U Mezzanine==4.2.2
  ・
  ・
Successfully installed Mezzanine-4.2.2 django-1.10.3 django-contrib-comments-1.7.3 filebrowser-safe-0.4.6 future-0.16.0 grappelli-safe-0.4.5 oauthlib-2.0.0 pillow-3.4.2 pytz-2016.7 requests-oauthlib-0.7.0 tzlocal-1.3
(akiyokoproject)$ python manage.py makemigrations
(akiyokoproject)$ python manage.py migrate

Mezzanine 系のモデル変更はありませんでしたが、Django 系の auth_user のモデルが少し変更になったようです。

$ sudo supervisorctl restart all


 

5. テンプレートの更新

HTMLファイル確認用の templats ディレクトリに、テンプレートの更新差分(4.1.0 → 4.2.2)をマージします。

config/settings.py

@@ -273,7 +275,7 @@ TEMPLATES = [
     {
         "BACKEND": "django.template.backends.django.DjangoTemplates",
         "DIRS": [
-            os.path.join(PROJECT_ROOT, "custom/templates"),
+            #os.path.join(PROJECT_ROOT, "custom/templates"),
             os.path.join(PROJECT_ROOT, "templates")
         ],
         "APP_DIRS": True,

上記のように修正して、一時的にカスタムテンプレートを外してから、

$ python manage.py collecttemplates

を実行して、テンプレートを上書きします。

なお、上書きするときにいちいちオーバーライドするかどうか聞かれるので、
~/.virtualenvs/akiyokoproject/lib/python2.7/site-packages/mezzanine/core/management/commands/collecttemplates.py

               self.stdout.write("Template exists%s.\n" % prev)
                #confirm = input("Overwrite?  (yes/no/abort): ")
                #while confirm not in ("yes", "no", "abort"):
                #    confirm = input(
                #        "Please enter either 'yes', 'no' or 'abort': ")
                #if confirm == "abort":
                #    self.stdout.write("Aborted\n")
                #    break  # exit templates copying loop
                #elif confirm == "no":
                #    self.stdout.write("[Skipped]\n")
                #    copy = False
            if copy:
                try:
                    os.makedirs(os.path.dirname(dest))
                except OSError:
                    pass
                shutil.copy2(path, dest)
                template_src[name] = app
                count += 1
        if verbosity >= 1:
            s = "s" if count != 1 else ""
            self.stdout.write("\nCopied %s template%s\n" % (count, s))

とコメントアウトすると、手間が省けます。


custom アプリケーションで独自に修正したテンプレートの一覧は以下の通りなので、

custom/__init__.py
custom/static/css/custom.css
custom/static/img/slide-1.jpg
custom/static/img/slide-2.jpg
custom/templates/base.html
custom/templates/blog/blog_post_detail.html
custom/templates/blog/blog_post_list.html
custom/templates/generic/includes/comment.html
custom/templates/generic/includes/comments.html
custom/templates/includes/form_fields.html
custom/templates/index.html
custom/templates/pages/menus/dropdown.html
custom/templates/twitter/tweets.html
custom/templatetags/__init__.py
custom/templatetags/add_attributes.py

本家テンプレートの更新差分(4.1.0 → 4.2.2)と重複したファイル

  • blog/blog_post_detail.html

については手動でマージをおこない、その他の

  • includes/editable_loader.html
  • pages/form.html
  • pages/menus/admin.html

については custom アプリケーションの templates ディレクトリ以下のものに上書きしました。

Mezzanine の本番設定 〜AWS 環境構築から運用設定まで〜(その4:Mezzanine の運用設定)

Mezzanine インフラ 運用 AWS

こんにちは、akiyoko です。

Mezzanine は、まだまだ日本での認知度はイマイチですが、知る人ぞ知る Python製の WordPress風フルスタックCMSフレームワークです。

akiyoko.hatenablog.jp


今年の 7月に、Mezzanine を使った某ブログサイト(将来的に ECサイトを増設予定)の本番運用を開始しました。*1 その備忘録として、AWS の初期設定から Mezzanine テーマのカスタマイズ、Mezzanine の本番デプロイ、ちょっとした運用設定までの記録をまとめておくことにしました。

全ての記録を一つの記事にすると長くなり過ぎるので、テーマごとに、

の 4本に記事を分割することにしました。

今回はラスト 4本目、「その4:Mezzanine の運用設定」について説明します。


Mezzanine の運用設定として実施した内容としては、

  • Mezzanine 本体のカスタム設定
  • Mezzanine サイトのページ設定
  • バックアップの設定
  • sitemap の定期更新
  • Google Search Console の設定
  • CloudWatch の監視設定

となります。



【目次】



 

1. Mezzanine 本体のカスタム設定

config/settings.py の設定を変更して、Mezzanine 本体のカスタム設定をおこないます。

本来この設定変更は本番デプロイ時(fab all コマンド実行時)かその直後におこなうべきですが、運用に関わる設定なのでここに記しておきます。

1.1. ブログ記事のフィーチャー・イメージ設定

ブログ記事にフィーチャー・イメージを付けるかどうかの設定です。
なお、画像は必須ではありません。

--- a/config/settings.py
+++ b/config/settings.py
@@ -79,7 +79,7 @@ from django.utils.translation import ugettext_lazy as _

 # Setting to turn on featured images for blog posts. Defaults to False.
 #
-# BLOG_USE_FEATURED_IMAGE = True
+BLOG_USE_FEATURED_IMAGE = True

 # If True, the django-modeltranslation will be added to the
 # INSTALLED_APPS setting.

 

1.2. レーティング設定

レーティング機能をオフにします。

--- a/config/settings.py
+++ b/config/settings.py
@@ -85,6 +85,9 @@ BLOG_USE_FEATURED_IMAGE = True
 # INSTALLED_APPS setting.
 USE_MODELTRANSLATION = False

+# Comment settings
+COMMENTS_USE_RATINGS = False
+

 ########################
 # MAIN DJANGO SETTINGS #


 

2. Mezzanine サイトのページ設定

Django Admin(/admin/)にログインして、Mezzanine サイトの各種設定を行います。

 

2.1. Settings

[Site]>[Settings]から Mezzanine サイト全体の設定を行います。

項目 内容 設定例
Comments > Accounts required for commenting コメントするのにアカウントが必要か チェックしない
Comments > Auto-approve comments コメントを自動承認するか チェック
Comments > Show removed comments 削除済みのコメントを表示するか チェックしない
Comments > Show unapproved comments 未承認のコメントを表示するか チェック
Twitter > Twitter OAuth access token Twitter 連携で必要 Twitter 側の設定 ① を参照
Twitter > Twitter OAuth access token secret 同上 Twitter 側の設定 ② を参照
Twitter > Twitter OAuth consumer key 同上 Twitter 側の設定 ③ を参照
Twitter > Twitter OAuth consumer secret 同上 Twitter 側の設定 ④ を参照
Twitter > Default Number of Tweets Tweet の表示数 5
Twitter > Default Twitter Query Twitter 検索のキー aki_yok (*2
Twitter > Default Twitter Query Type Twitter 検索キーの種類 User
Miscellaneous > Akismet API Key Akismet 連携で必要 Akismet 側の設定を参照
Miscellaneous > Blog posts per page ブログ一覧の表示数 6
Miscellaneous > Google Analytics ID Google Analytics 連携で必要 Google Analytics 側の設定を参照
Miscellaneous > Max paging links ブログ一覧のページ表示数 10
Miscellaneous > Accounts required for rating レーティングするのにアカウントが必要か Yes
Miscellaneous > Rich Text filter level Rich Text ページのフィルタリングレベル No filtering (*3

f:id:akiyoko:20161029182952p:plain



Twitter 側の設定

f:id:akiyoko:20161030115540p:plain


(参考)Twitter APIの使い方まとめ


Akismet 側の設定

f:id:akiyoko:20161030115703p:plain


Google Analytics 側の設定

f:id:akiyoko:20161030122212p:plain


 

2.2. Groups

ブログ投稿しかできない「akiyoko」ユーザを作成するため、まず、ブログ投稿関連のパーミッションのみを許可した「Blog user」という権限グループを作成します。


以下のパーミッションを選択します。

  • blog | Blog Category | Can add Blog Category
  • blog | Blog Category | Can change Blog Category
  • blog | Blog Category | Can delete Blog Category
  • blog | Blog post | Can add Blog post
  • blog | Blog post | Can change Blog post
  • blog | Blog post | Can delete Blog post
  • galleries | Gallery | Can add Gallery
  • galleries | Gallery | Can change Gallery
  • galleries | Gallery | Can delete Gallery
  • galleries | Image | Can add Image
  • galleries | Image | Can change Image
  • galleries | Image | Can delete Image

f:id:akiyoko:20161030124441p:plain


 

2.3. Users

「akiyoko」ユーザを作成して、「Blog user」権限グループを付与します。

f:id:akiyoko:20161030124507p:plain


ここで、新しく追加したユーザで Django Admin サイトにログインしようとすると「You don't have permission to access the admin for this site.」というエラーが出てログインできない問題が発生(2016年7月時点)。 *4

関連するデータを見てみると、

mysql> select * from core_sitepermission;
+----+---------+
| id | user_id |
+----+---------+
|  1 |       2 |
+----+---------+
1 row in set (0.00 sec)

mysql> select * from core_sitepermission_sites;
Empty set (0.00 sec)

となっていたのですが、本事象が発生しなかった開発環境(Vagrant)では、

mysql> select * from core_sitepermission;
+----+---------+
| id | user_id |
+----+---------+
|  1 |       2 |
+----+---------+
1 row in set (0.00 sec)

mysql> select * from core_sitepermission_sites;
+----+-------------------+---------+
| id | sitepermission_id | site_id |
+----+-------------------+---------+
|  1 |                 1 |       1 |
+----+-------------------+---------+
1 row in set (0.00 sec)

となっていて、結局原因はよく分からなかったのですが、

mysql> insert core_sitepermission_sites(id,sitepermission_id,site_id) values(1,1,1);

と実行して core_sitepermission_sites レコードを作成してやると、事象は発生しなくなりました。


 

2.4. Contact

お問い合わせページの作成方法。こんな感じのページです。

f:id:akiyoko:20161030130048p:plain

[Content]>[Pages]から[Add Form]を選択して、Form ページを作成。

項目 内容
Title Contact
Content Please fill out the form below and submit for your inquiry.
Button text Submit
Email > Send email to user チェックしない
Email > From address no-reply@akiyoko.com
Email > Send email to others xxx@gmail.com, xxx@gmail.com
Email > Subject [akiyoko.com] New contact
Email > Message 問い合わせフォームからコンタクトがありました。対応をお願いします。
Fields > Name Single line text
Fields > Email Email
Fields > Message Multi line text


f:id:akiyoko:20161029183251p:plain


実際にフォームへの問い合わせがあれば、[Email > Send email to others]に設定したメールアドレス宛に、以下のような内容のメールが届きます。

問い合わせフォームからコンタクトがありました。対応をお願いします。

Name:
xxx

Email:
xxx@xxx.com

Message:
メッセージの内容


http://akiyoko.com


 

3. バックアップの設定

Mezzanine サイトでアップロードした画像ファイルは、

/home/webapp/mezzanine/akiyokoproject/static/media/
└── uploads
    └── blog
        └── test.png

というパスに配置されるので、以下のように media ディレクトリ以下を圧縮してバックアップします。

$ tar czvf <出力ファイル名> <対象ファイル or 対象ディレクトリ>

$ tar czvf /tmp/media_backup_`date +%y%m%d%H%M`.tar.gz /home/webapp/mezzanine/akiyokoproject/static/media

圧縮したファイル名は「media_backup_1610301815.tar.gz」などとなります。


また、MySQL のバックアップは以下のようにして実行します。

$ mysqldump --single-transaction -u root -p akiyokoproject > /tmp/mysql_backup_`date +%y%m%d%H%M`.dump

こちらは今のところ圧縮しない方針です(ファイルがもっと大きくなれば圧縮した方がよさそうですが)。


以下、バックアップファイルを cron で毎日深夜に S3 にアップロードするまでの手順です。

 

3.1. S3 バケットの準備

バックアップ用バケット(バケット名は「akiyokoproject」とします)を作成し、有効期限ルールを設定します。


バケットの Properties の[Lifecycle]から、[Add rule]を選択。
f:id:akiyoko:20161030201246p:plain

バケット内の「media/」ディレクトリ以下のオブジェクトの有効期限を10日間(有効期限を過ぎたものは削除)に設定します。
f:id:akiyoko:20161030201305p:plain

f:id:akiyoko:20161030201323p:plain

f:id:akiyoko:20161030201340p:plain

「db/」ディレクトリ以下のオブジェクトの有効期限についても同様に設定します。


(参考)Amazon S3でオブジェクトの有効期限を設定できるようになりました | Developers.IO


 

3.2. AWS CLI のインストール

AWS CLI を pip でインストールします。

(akiyokoproject)$ pip install awscli

(akiyokoproject)$ pip list | grep awscli
awscli (1.11.10)


 

3.3. 実行シェルの作成

media ディレクトリ以下のファイルと MySQL のバックアップを行う、シェルを作成します。

/home/webapp/mezzanine/akiyokoproject/scripts/backup.sh

#!/bin/sh

MEDIA_BACKUP_FILE=media_backup_`date +%y%m%d%H%M`.tar.gz
DB_BACKUP_FILE=mysql_backup_`date +%y%m%d%H%M`.dump
DB_PASS=xxxxxxxx

# Create backup file
tar czvf /tmp/$MEDIA_BACKUP_FILE /home/webapp/mezzanine/akiyokoproject/static/media
mysqldump --single-transaction -u root -p$DB_PASS akiyokoproject > /tmp/$DB_BACKUP_FILE

# Copy file to S3
. /home/webapp/.virtualenvs/akiyokoproject/bin/activate
aws s3 cp /tmp/$MEDIA_BACKUP_FILE s3://akiyokoproject/media/
aws s3 cp /tmp/$DB_BACKUP_FILE s3://akiyokoproject/db/

(mysqldump のパスワード「xxxxxx」の部分は、適宜設定してください。)

$ sudo chmod 755 /home/webapp/mezzanine/akiyokoproject/scripts/backup.sh


(参考)


 

3.4. cron ファイルの編集

デプロイ時に Fabric スクリプトを実行すると、「/etc/cron.d/」配下に「akiyokoproject」という cron ファイルが用意されるので、このファイルに定期バックアップルールを追記します。

$ ls -al /etc/cron.d/
total 16
drwxr-xr-x  2 root root   4096 Oct 30 19:52 .
drwxr-xr-x 92 root root   4096 Aug 11 21:59 ..
-rw-------  1 root webapp  306 Oct 30 19:52 akiyokoproject
-rw-r--r--  1 root root    102 Feb  9  2013 .placeholder

/etc/cron.d/akiyokoproject

# Poll Twitter every 5 minutes
# Comment-out if you don't use Mezzanine's Twitter app
*/5 * * * * webapp /home/webapp/.virtualenvs/akiyokoproject/bin/python /home/webapp/mezzanine/akiyokoproject/manage.py poll_twitter

(poll_twitter を5分ごとに実行する設定が既に書き込まれています。)



毎日深夜 0時5分に定期バックアップのためのシェルが実行されるような設定を、末尾に追加します。

# Daily backup
5 0 * * * webapp /home/webapp/mezzanine/akiyokoproject/scripts/backup.sh


(参考)cron設定をもっとわかりやすく説明! - こまけぇこたぁいんだよ


 

3.5. cron 実行ログの出力先を変更

Ubuntu はデフォルトで、cron の実行ログが「/var/log/syslog」に出力されるようになっているのですが、「/var/log/cron.log」に出力されるように rsyslog の設定を変更しておきます。


/etc/rsyslog.d/50-default.conf

  ・
  ・
(略)

# First some standard log files.  Log by facility.
#
auth,authpriv.*                 /var/log/auth.log
*.*;auth,authpriv.none          -/var/log/syslog
#cron.*                         /var/log/cron.log
#daemon.*                       -/var/log/daemon.log
kern.*                          -/var/log/kern.log
#lpr.*                          -/var/log/lpr.log
mail.*                          -/var/log/mail.log
#user.*                         -/var/log/user.log

(略)
  ・
  ・

となっているのを、「cron.*」のコメントを外して、

cron.*                         /var/log/cron.log

と修正します。

最後に、

$ sudo service rsyslog restart

を実行して、rsyslog を再起動します。
これで、/var/log/cron.log にログが出力されるようになりました。


(参考)ubuntu14.04 cronログ出力設定 - Qiita



 

4. sitemap の定期更新

Mezzanine には、Google Search Console の設定に必要な「sitemap.xml」を Google に送信するためのコマンドが用意されています。 *5

(akiyokoproject)$ python manage.py ping_google
System check identified some issues:

WARNINGS:
?: (1_8.W001) The standalone TEMPLATE_* settings were deprecated in Django 1.8 and the TEMPLATES dictionary takes precedence. You must put the values of the following settings into your default TEMPLATES dict: TEMPLATE_DIRS.

WARNING が出ているのは無視するとして、何も反応が無いのが気になります。。


先述の cronファイルの末尾に、以下のルールを追加します。

/etc/cron.d/akiyokoproject

# Hourly ping google
0 * * * * webapp /home/webapp/.virtualenvs/akiyokoproject/bin/python /home/webapp/mezzanine/akiyokoproject/manage.py ping_google


 

5. Google Search Console の設定

ここでは細かく書きませんが、Google Search Console の各種設定を行い、SEO 対策をしておきます。


(参考)


 

6. CloudWatch の監視設定

CloudWatch で以下の監視を実施し、異常を検知した場合に管理者用のメールアドレスに通知をするように設定をおこないます。

  • EC2 インスタンスの CPU使用率監視
  • EC2 インスタンスの死活監視

なお、CloudWatch の監視については、無料枠内で最低限の監視ができればよいという方針で運用しています。


(参考)


 

6.1. 監視通知用の SNS 作成

SNS の通知先に、監視用のメールアドレスを指定します。


まず、[Topics]から[Create new topic]を選択してトピックを作成します。

f:id:akiyoko:20161102004922p:plain

トピック名は「akiyokoproject-notification」としました。 *6

f:id:akiyoko:20161102004949p:plain

作成されたトピックの ARN をクリックして、[Create subscription]をクリックします(あるいは[Subscription]から[Create subscription]をクリックしてから ARN を直接指定します)。

f:id:akiyoko:20161102005016p:plain
f:id:akiyoko:20161102005041p:plain

Protocol Email
Endpoint 管理者のメールアドレス

を設定して、[Create subscription]をクリックします。

f:id:akiyoko:20161102005103p:plain

確認メールが届くので、メール本文の「Confirm subscription」をクリックします。

f:id:akiyoko:20161102005128p:plain

f:id:akiyoko:20161102005141p:plain

「Subscription ID」が「PendingConfirmation」から ARN に変われば、設定完了となります。

f:id:akiyoko:20161102005156p:plain


 

6.2. メトリクスを設定

[Metrics]から[EC2]をクリックします。
f:id:akiyoko:20161102011342p:plain
[Per-Instance Metrics]をクリック。
f:id:akiyoko:20161102011417p:plain


「CPUUtilization」(CPU使用率)および「StatusCheckFailed」(インスタンスステータス異常あるいはシステムステータス異常)を選択します。
f:id:akiyoko:20161102011447p:plain

(参考)【小ネタ】CloudWatchの「StatusCheckFailed_Instance」と「StatusCheckFailed_System」について | Developers.IO



続けて、作成したメトリクスをダッシュボードに保存します。


[Actions]から、[Add to dashboard]を選択します。
f:id:akiyoko:20161102082216p:plain

ダッシュボード名を入力して、[Add to dashboard]ボタンをクリック。
f:id:akiyoko:20161102082310p:plain

[Save dashboard]ボタンをクリックして完了です。
f:id:akiyoko:20161102082349p:plain




 

6.3. アラームを設定

  • EC2 インスタンスの平均 CPU 使用率が 70% を超える状態が 10分以上継続
  • EC2 インスタンスのインスタンスステータス異常あるいはシステムステータス異常が 10分以上継続

を閾値として、アラームを作成します。 *7


まずは、「CPUUtilization」(CPU使用率)のアラームから。


「Graphed metrics」タブから、「CPUUtilization」の右側のベルアイコンをクリックします。
f:id:akiyoko:20161102011732p:plain

以下のようにアラームの設定をします。
f:id:akiyoko:20161102013453p:plain


(参考)E メールを送信する CPU 使用率アラームの作成 - Amazon CloudWatch



引き続き、「StatusCheckFailed」(インスタンスステータス異常あるいはシステムステータス異常)の右側のベルアイコンをクリックして、アラームを以下のように設定します。
f:id:akiyoko:20161102012644p:plain


 

TODO

その他の運用設定として、

  • logrotate の設定が不完全(application.log とか)

について、今後対応していく予定です。



 

まとめ

今回は、「Mezzanine の本番設定」と銘打った連続記事の最後を締めくくる「Mezzanine の運用設定」として、

  • Mezzanine 本体のカスタム設定
  • Mezzanine サイトのページ設定
  • バックアップの設定
  • sitemap の定期更新
  • Google Search Console の設定
  • CloudWatch の監視設定

を実施した内容を記載しました。


Mezzanine の本番サイトを運用し始めて 3ヶ月ほど経ちますが、やりたいこと、やらなければいけないことがまだまだ盛り沢山です。

今後も定期的に、Mezzanine の記事をアップしていこうと思います。



 

参考本

運用についてはまだまだ勉強不足です。。

Amazon Web Services 定番業務システム12パターン 設計ガイド

Amazon Web Services 定番業務システム12パターン 設計ガイド

Amazon Web Services パターン別構築・運用ガイド  一番大切な知識と技術が身につく

Amazon Web Services パターン別構築・運用ガイド  一番大切な知識と技術が身につく

  • 作者: NRIネットコム株式会社,佐々木拓郎,林晋一郎,小西秀和,佐藤瞬
  • 出版社/メーカー: SBクリエイティブ
  • 発売日: 2015/03/25
  • メディア: Kindle版
  • この商品を含むブログを見る

Amazon Web Servicesクラウドサーバ構築ガイド コストを削減する導入・実装・運用ノウハウ

Amazon Web Servicesクラウドサーバ構築ガイド コストを削減する導入・実装・運用ノウハウ

*1:Mezzanine を採用したのは、個人の趣味です。

*2:「@」は付けない

*3:「No filtering」に設定すると、Rich Text ページ内のタグが無効化されなくなる

*4:(関連)https://groups.google.com/forum/#!topic/mezzanine-users/RSoPDeDrnaw

*5:正確には Django の機能です。(参考)django/ping_google.py at 1.9.7 · django/django · GitHub

*6:管理者のメールアドレスに通知する系の SNS は、「akiyokoproject-notification」を使い回す予定です。

*7:10分以上継続というのは、5分間の監視インターバルを 2回連続で異常が検知された場合を指す

Mezzanine の本番設定 〜AWS 環境構築から運用設定まで〜(その3:Mezzanine の本番デプロイ)

Django Mezzanine MySQL Nginx

こんにちは、akiyoko です。

Mezzanine は、知る人ぞ知る Python製の WordPress風フルスタックCMSフレームワークです。

akiyoko.hatenablog.jp


今年の 7月に、Mezzanine を使った某ブログサイト(将来的に ECサイトを増設予定)の本番運用を開始しました。*1 その備忘録として、AWS の初期設定から Mezzanine の本番デプロイ、ちょっとした運用設定までの記録をまとめておくことにしました。

全ての記録を一つの記事にすると長くなり過ぎるので、テーマごとに、

の 4本に記事を分割することにしました。

今回はその 3本目、「その3:Mezzanine の本番デプロイ」について説明します。


Mezzanine の本番デプロイとして実施した内容としては、

  • 本番サーバ(Ubuntu)を起動
  • Fabric スクリプトを実行
  • Fabric スクリプト実行後の各種設定
    • SSL の設定
    • URL の転送設定
    • BASIC認証の設定
    • MySQL のインストール
    • メールの設定
    • ログの設定
    • タイムゾーンを Asia/Tokyo に変更
    • sitemap.xml / robot.txt の設定

となります。



【目次】


 

1. 概要

Mezzanine には何と、コマンド一発で リバースプロキシや DB などを含めた全部入りの本番環境が構築できる Fabric スクリプト が含まれています。

Fabric

Each Mezzanine project comes bundled with utilities for deploying production Mezzanine sites, using Fabric. The provided fabfile.py contains composable commands that can be used to set up all the system-level requirements on a new Debian based system, manage each of the project-level virtual environments for initial and continuous deployments, and much more.


(akiyoko 翻訳)
それぞれの Mezzanine プロジェクトには、本番 Mezzanine サイトをデプロイするための Fabric を使った便利ツールがバンドルされています。提供されている fabfile.py には、新しい Debian ベースのシステム上のあらゆるシステムレベルの依存ライブラリをセットアップしたり、初回デプロイ・継続的デプロイのためのプロジェクトレベルの仮想環境を管理したり、他にもたくさんのことをしたりするために利用できる組み立て可能なコマンド群が含まれています。


Deployment — Mezzanine 4.2.2 documentation


 
Mezzanine の Fabric スクリプト (*2) を実行するには、Fabric を pip でインストールした後、

$ fab secure

してから、

$ fab all

するだけです。簡単ですよね!


一度試してみれば分かりますが、もう至れり尽くせりです。
例えば、HTTPS のためのオレオレ証明書を作成して所定の位置に配置し、Nginx の設定までしてくれるという心尽くしに「ありがとう!」と思わず声が漏れてしまいます。



Mezzanine デフォルトの Fabric スクリプトを実行して構築した本番環境は、以下のコンポーネントで構成されます。

  • NGINX - public facing web server
  • gunicorn - internal HTTP application server
  • PostgreSQL - database server
  • memcached - in-memory caching server
  • supervisord - process control and monitor
  • virtualenv - isolated Python environments for each project
  • git or mercurial - version control systems (optional)


Deployment — Mezzanine 4.2.2 documentation


データベースとして PostgreSQL がインストールされますが、PostgreSQL と MySQL はどっちがいいの?という議論(*3)は置いておいて、私の場合は単純に「操作や運用に慣れている」という理由から、今回はデータベースを PostgreSQL から MySQL に入れ替えることにしました(実際には、Fabric スクリプトを実行して PostgreSQL を一旦インストールした後に手動で削除し、Ansible で MySQL をインストールしました)。


(参考)


 
最終的なサーバとコンポーネント、プロジェクトの構成は以下のようになりました。

サーバ:

  • Ubuntu 14.04.4 LTS


コンポーネント:

Nginx 1.4.6
Gunicorn 19.6.0
MySQL 5.5.49
Supervisor 3.0b2
Python 2.7.6
Django 1.9.7
Mezzanine 4.1.0 (*5

上記のバージョンは、いずれも 2016年7月時点のものです。


プロジェクト:

プロジェクト名 akiyokoproject
プロジェクトの配置場所 /home/webapp/mezzanine/akiyokoproject
プロジェクトのオーナ(Ubuntu ユーザ名) webapp
virtualenv の配置場所 /home/webapp/.virtualenvs/akiyokoproject
デプロイ先のドメイン名 akiyoko.com (*6


以降、Fabric スクリプトの実行を含めた、Mezzanine の本番デプロイの手順について説明します。


 

2. Ubuntu インスタンスを起動

Fabric スクリプトを実行する前に、スクリプトを流し込むサーバを立ち上げる必要があります。サーバには、AWS EC2 で起動した Ubuntu インスタンスを利用します。

また初期設定として、Fabric スクリプトを流し込むための SSHユーザの設定などを行います。

 

2.1. EC2インスタンスを起動

AWS EC2インスタンスを立ち上げます。その際、IAMロール「ec2-prod」(*7)を付けておきます。

具体的な起動方法については、以下の過去記事を参照してください。

<過去記事>
akiyoko.hatenablog.jp


以下、インスタンスの IPアドレスを「52.xxx.xxx.xxx」とします。

2.2. webapp ユーザを作成

以下のコマンドで Ubuntu にアクセスします。
(サーバにアクセスするための SSH秘密鍵が「~/.ssh/aws_p1.pem」に配置されているという前提です。)

$ ssh -i ~/.ssh/aws_p1.pem ubuntu@52.xxx.xxx.xxx

Fabric スクリプトの各コマンドを実行する SSHユーザ「webapp」を作成します。

$ sudo adduser --gecos '' webapp

(ここでは、パスワードは「pass」など簡単なもので OK。)

(参考)What do the `--disabled-login` and `--gecos` options of `adduser` command stand for? - Ask Ubuntu


次に、webapp ユーザがパスワード無しでログインできるように設定します。

$ sudo visudo -f /etc/sudoers.d/90-cloud-init-users

を実行して、以下の一行を末尾に追加します(control + option + x で保存)。

webapp ALL=(ALL) NOPASSWD:ALL

Ubuntu サーバを再起動後、ubuntu ユーザのものと同じ SSH鍵でログインできるように設定します。 *8

$ sudo service sudo restart

$ sudo su - webapp
$ mkdir -m 700 ~/.ssh
$ sudo cp -a /home/ubuntu/.ssh/authorized_keys ~/.ssh/
$ sudo chown webapp. ~/.ssh/authorized_keys
$ sudo chmod 600 ~/.ssh/authorized_keys

最後に、Mac 上から

$ ssh -i ~/.ssh/aws_p1.pem webapp@52.xxx.xxx.xxx

でサーバにログインできることが確認できれば OK。




 

3. Fabric スクリプトを実行

3.1. 本番デプロイ用の Mezzanine プロジェクトを作成

Mac上で、Fabric スクリプトを実行するための、本番デプロイ用の Mezzanine プロジェクトを作成します。

### virualenv が Mac にインストール済みという前提
$ virtualenv ~/.venvs/mezzanine_deploy
$ source ~/.venvs/mezzanine_deploy/bin/activate

### Fabric を実行するために必要なコンポーネントを pip install
(mezzanine_deploy)$ pip install future fabric mezzanine

Mezzanine (4.1.0)
Fabric (1.11.1)
paramiko (1.17.0)

### 適当なディレクトリにプロジェクトを作成
(mezzanine_deploy)$ cd ~/dev
(mezzanine_deploy)$ mkdir mezzanine_deploy_akiyokoproject_for_aws && cd $_
(mezzanine_deploy)$ mezzanine-project config .


本番デプロイ用に、config/local_settings.py を以下のように編集します。 *9

    ・
    ・
###################
# DEPLOY SETTINGS #
###################

# Domains for public site
ALLOWED_HOSTS = [".akiyoko.com"]

# These settings are used by the default fabfile.py provided.
# Check fabfile.py for defaults.

FABRIC = {
    "SSH_USER": "webapp",  # VPS SSH username and will be an application owner
    "SSH_KEY_PATH": "~/.ssh/aws_p1.pem",
    "HOSTS": ["52.xxx.xxx.xxx"],  # The IP address of your VPS
    "DOMAINS": ["akiyoko.com"],  # Edit domains in ALLOWED_HOSTS
    #"LIVE_HOSTNAME": "akiyoko.com", # Host for public site.
    #"VIRTUALENV_HOME": "/home/ubuntu/.virtualenvs",  # Absolute remote path for virtualenvs
    "PROJECT_NAME": "akiyokoproject", # Unique identifier for project
    "REQUIREMENTS_PATH": "requirements.txt",  # Project's pip requirements
    "GUNICORN_PORT": 8000, # Port gunicorn will listen on
    "LOCALE": "en_US.UTF-8",  # Should end with ".UTF-8"
    "DB_PASS": "dbpass",  # Live database password
    "ADMIN_PASS": "adminpass",  # Live admin user password
    "SECRET_KEY": SECRET_KEY,
    "NEVERCACHE_KEY": NEVERCACHE_KEY,
}

(「ADMIN_PASS」および「ADMIN_PASS」の値はサンプルですので、コピペしないようにご注意ください。)


ちなみに、

    "DEPLOY_TOOL": "git",

と設定すると、

Fatal error: local() encountered an error (return code 128) while executing 'git push -f ssh://ubuntu@52.xxx.xxx.xxx/home/ubuntu/git/akiyokoproject.git master'

というエラーになったので、今回は設定していません。


 

3.2. fabfile.py を修正

2016年7月時点では、fab all 実行時に、

$ /home/webapp/mezzanine/akiyokoproject/bin/python /home/webapp/mezzanine/akiyokoproject/manage.py syncdb --noinput ->

[52.xxx.xxx.xxx] out: Unknown command: 'syncdb'
[52.xxx.xxx.xxx] out: Type 'manage.py help' for usage.
[52.xxx.xxx.xxx] out:

Fatal error: run() received nonzero return code 1 while executing!

というエラーが出たので、fabfile.py を以下のように修正しました。 *10


fabfile.py

@@ -435,6 +435,7 @@ def install():
     """
     # Install system requirements
     sudo("apt-get update -y -q")
+    sudo("apt-get upgrade -y -q")
     apt("nginx libjpeg-dev python-dev python-setuptools git-core "
         "postgresql libpq-dev memcached supervisor python-pip")
     run("mkdir -p /home/%s/logs" % env.user)
@@ -635,7 +636,7 @@ def deploy():
             rsync_upload()
     with project():
         manage("collectstatic -v 0 --noinput")
-        manage("syncdb --noinput")
+        manage("makemigrations --noinput")
         manage("migrate --noinput")
     for name in get_templates():
         upload_template_and_reload(name)

原因は、fabfile.py 内で使用されている syncdb コマンドが Django 1.9(Django 1.7 以降)に対応していないからです。

(参考)Deploy Mezzanine to Ubuntu 14 server on DigitalOcean with Fabric | PerezProgramming


なお、今回のように AWS + Ubuntu でサーバを構築する場合は、fab secure の内容はほぼ網羅できているのでスキップした(fab secure コマンドは実行しなかった)のですが、それだと「apt-get upgrade -y -q」が唯一実行できていないので、fabfile.py の install() に追加しています。


 

3.3. デプロイ実行

先述のように、今回は AWS + Ubuntu でサーバを構築したので、

(mezzanine_deploy)$ fab secure

は今回実行しません。


以下のコマンドのみを実行します。

(mezzanine_deploy)$ fab all

正常に Fabric スクリプトが流れ終わったら、デプロイ後の後処理を実施していきます。


 

4. SSL の設定

ここではすでに、正規の SSL証明書を取得しているものとします。
取得方法については、以下の過去記事を参考にしてください。

<過去記事>
akiyoko.hatenablog.jp



まずは、「GoGetSSL」の管理ページから、www_akiyoko_com.crt(SSL証明書)と www_akiyoko_com.ca-bundle(中間証明書)をダウンロードしておきます。


クライアント(Mac)からサーバに SSL証明書と中間証明書を転送します。

$ scp -i ~/.ssh/aws_p1.pem ~/Downloads/www_akiyoko_com.crt ubuntu@52.xxx.xxx.xxx:/tmp/
$ scp -i ~/.ssh/aws_p1.pem ~/Downloads/www_akiyoko_com.ca-bundle ubuntu@52.xxx.xxx.xxx:/tmp/

次に、Ubuntu 上で、SSL証明書を(Fabric スクリプトが作成してくれたデフォルトのものから)入れ替えます。

$ sudo mv /etc/nginx/conf/akiyokoproject.crt /etc/nginx/conf/akiyokoproject.crt.orig
$ sudo mv /etc/nginx/conf/akiyokoproject.key /etc/nginx/conf/akiyokoproject.key.orig
$ sudo cp /tmp/www_akiyoko_com.crt /etc/nginx/conf/akiyokoproject.crt
$ sudo chmod 644 /etc/nginx/conf/akiyokoproject.crt

### 中間証明書を連結する
$ cat /tmp/www_akiyoko_com.ca-bundle | sudo tee -a /etc/nginx/conf/akiyokoproject.crt

なお、Android 実機(Nexus 5)から HTTPS で閲覧したときに証明書エラーが出たので、以下を参考に、SSL証明書に中間証明書を連結しました。 *11

(参考)



最後に、秘密鍵を書き換えて(*12)、Nginx をリロードします。

$ sudo vi /etc/nginx/conf/akiyokoproject.key
(秘密鍵をコピペ)

$ ls -al /etc/nginx/conf
total 24
drwxr-xr-x 2 root root 4096 Jul  7 15:29 .
drwxr-xr-x 6 root root 4096 Jul  7 15:14 ..
-rw-r--r-- 1 root root 1964 Jul  7 15:29 akiyokoproject.crt
-rw-r--r-- 1 root root 1111 Jul  7 15:14 akiyokoproject.crt.orig
-rw-r--r-- 1 root root 1704 Jul  7 15:29 akiyokoproject.key
-rw-r--r-- 1 root root 1704 Jul  7 15:14 akiyokoproject.key.orig

### Nginx をリロード
$ sudo service nginx reload


 

5. URL の転送設定

ここで、

http://akiyoko.com/foo/bar ⇒ https://akiyoko.com/foo/bar
http://www.akiyoko.com/foo/bar ⇒ https://akiyoko.com/foo/bar
https://www.akiyoko.com/foo/bar ⇒ https://akiyoko.com/foo/bar

のように URL転送(要するに全部 https の wwwなしドメインに転送)したいので、Nginx の設定を修正して 80 番ポートアクセス時の転送先を変更します。


/etc/nginx/sites-enabled/akiyokoproject.conf

server {

    #listen 80;
     listen 443 ssl;

    ・
    ・

server {
    listen 80;
    server_name akiyoko.com;
    return 301 https://akiyoko.com$request_uri;
}

server {
    listen 80;
    listen 443 ssl;
    server_name www.akiyoko.com;
    return 301 https://akiyoko.com$request_uri;
}

なお、以下 ↓ のように設定しても動作しましたが、上記の方がベターかと。

server {

    #listen 80;
     listen 443 ssl;

    ・
    ・

server {
    listen 80;
    server_name akiyoko.com;
    return 301 https://akiyoko.com$request_uri;
}

server {
    server_name www.akiyoko.com;
    return 301 https://akiyoko.com$request_uri;
}

一旦、Nginx をリロードします。

$ sudo service nginx reload


 

6. BASIC認証の設定

(デプロイ時はまだリリース前だったので)ここで一旦、BASIC認証をかけておきます。もちろん、サイト公開時には設定を外しておくのを忘れずに。

$ sudo apt-get -y install apache2-utils
$ sudo htpasswd -c /etc/nginx/.htpasswd test
(パスワードを入力)


Nginx の設定を変更します。

/etc/nginx/sites-enabled/akiyokoproject.conf

server {

    #listen 80;
     listen 443 ssl;
    server_name akiyoko.com;

    ・
    ・

    location / {
        auth_basic "Restricted";
        auth_basic_user_file /etc/nginx/.htpasswd;

        proxy_redirect      off;
        proxy_set_header    Host                    $host;
        proxy_set_header    X-Real-IP               $remote_addr;
        proxy_set_header    X-Forwarded-For         $proxy_add_x_forwarded_for;
        proxy_set_header    X-Forwarded-Protocol    $scheme;
        proxy_pass          http://akiyokoproject;
    }}


Nginx をリロードして作業完了です。

$ sudo service nginx reload


(参考)Nginx で Basic 認証 - Qiita



 

7. MySQL のインストール

先述の通り今回は、Fabric スクリプトを実行してインストールされる PostgreSQL から MySQL にデータベースを入れ替えました。実際の作業としては、インストールされた PostgreSQL を手動で削除し、Ansible スクリプトを実行して MySQL をインストールしました。

以下、その手順です。

7.1. PostgreSQL のアンインストール

まず、手動で PostgreSQL を削除します。

$ sudo apt-get -y --purge remove postgresql\*
$ sudo rm -rf /etc/postgresql/
$ sudo rm -rf /etc/postgresql-common/
$ sudo rm -rf /var/lib/postgresql/
$ sudo userdel -r postgres
$ sudo groupdel postgres


 

7.2. MySQL のインストール

次に、MySQL を Ansible を使って Mac からインストールします。
今回は、Ansible Galaxy を使ってインストールをおこないました。

<過去記事>
akiyoko.hatenablog.jp



Mac 上の適当なディレクトリにプロジェクトを作成します。

$ cd ~/dev/ansible-mysql

hosts

[db-servers]
52.xxx.xxx.xxx

site.yml

- hosts: db-servers
  user: ubuntu
  vars_files:
    - vars/database.yml
  roles:
    - { role: ANXS.mysql }

vars/database.yml

mysql_current_root_password: ''
mysql_root_password: dbpass
mysql_databases:
  - name: akiyokoproject
mysql_users:
  - name: akiyokoproject
    pass: akiyokoprojectpass
    priv: "akiyokoproject.*:ALL"

(「mysql_root_password」および「pass」の値はサンプルですので、コピペしないようにご注意ください。)



Ansible を実行します。

$ ansible-playbook -i hosts site.yml --sudo --private-key ~/.ssh/aws_p1.pem -vv

 

7.3. MySQL 用の設定

データベースを MySQL に変更したのに伴い、Ubuntu サーバの各種設定を変更します。


MySQL 用のプラグインをインストールします。

$ sudo apt-get -y install libmysqlclient-dev
$ pip install MySQL-python



settings.py のデータベース設定を修正します。

config/local_settings.py

    ・
    ・
DATABASES = {
    "default": {
        # Ends with "postgresql_psycopg2", "mysql", "sqlite3" or "oracle".
        "ENGINE": "django.db.backends.mysql",
        # DB name or path to database file if using sqlite3.
        "NAME": "akiyokoproject",
        # Not used with sqlite3.
        "USER": "akiyokoproject",
        # Not used with sqlite3.
        "PASSWORD": "akiyokoprojectpass",
        # Set to empty string for localhost. Not used with sqlite3.
        "HOST": "127.0.0.1",
        # Set to empty string for default. Not used with sqlite3.
        "PORT": "",
    }
}
    ・
    ・

データベースのマイグレーションを実行します。

$ python manage.py migrate
$ python manage.py createsuperuser

username (leave blank to use 'webapp'): admin
Email address: admin@akiyoko.com
Password: 
Password (again):


とりあえずデータベースをバックアップしておきます。

$ mkdir ~/db_backup
$ mysqldump --single-transaction -u root -p akiyokoproject > ~/db_backup/akiyokoproject_init.dump

### タイムスタンプをバックアップファイル名に含める場合
$ mysqldump --single-transaction -u root -p akiyokoproject > ~/db_backup/akiyokoproject_backup_`date +%y%m%d%H%M`.dump

### リストアするときのコマンド
### mysql -u root -p akiyokoproject < ~/db_backup/akiyokoproject_init.dump

(参考)MySQLのデータベースをmysqldumpでバックアップ/復元する方法 | WEB ARCH LABO



最後に gunicorn を再起動して完了です。

(akiyokoproject)$ python manage.py collectstatic
$ sudo supervisorctl restart all




ここで万が一(バックアップした AMI から起動した直後など?)、virtualenvwrapper を使って workon しようとした際に、

$ workon akiyokoproject
workon: command not found

というエラーが出てしまう場合は、一旦 Ubuntu からログアウトして、ubuntuユーザで接続し直してから、

$ sudo su - webapp

として .bashrc を読み込み直すとよさそうです(source ~/.bashrc としてもよかったのかな??)。



 

8. メールの設定

前提として、AWS Management Console で SES の設定が完了し、起動した EC2インスタンスに SES の権限を持った IAMロール(今回は「ec2-prod」がそれに相当)が付与されているものとします。

<過去記事>
akiyoko.hatenablog.jp


なお、Amazon SES を利用するため、postfix などの MTA のインストールは不要です。


(参考)



Ubuntuサーバ上で、django-ses をインストールします。

(akiyokoproject)$ pip install django-ses


config/local_settings.py の末尾に以下を追加します。

################
# AWS SETTINGS #
################
#AWS_ACCESS_KEY_ID = "AKIxxxxxxxxxxxxxxxxx"
#AWS_SECRET_ACCESS_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

##################
# EMAIL SETTINGS #
##################
EMAIL_BACKEND = "django_ses.SESBackend"
AWS_SES_REGION_NAME = "us-west-2"
AWS_SES_REGION_ENDPOINT = "email.us-west-2.amazonaws.com"
#EMAIL_HOST_USER = "admin@akiyoko.com"
#EMAIL_USE_TLS = True
#EMAIL_HOST = "email-smtp.us-west-2.amazonaws.com"
#DEFAULT_FROM_EMAIL = SERVER_EMAIL = u"no-reply <no-reply@akiyoko.com>"

上記は、SES を「Oregon」リージョンで開設した場合の設定になります。

なお、コメントアウト部分はあってもなくても OK なのですが、「EMAIL SETTINGS」に設定している 3つは最低限必要となります。 *13

あと、「AWS_SES_REGION_ENDPOINT」は、普段(?)のホスト名とは違う(「email.」で始まる)ので注意が必要かと。


(参考)Customizing Mezzanine | ROSS LAIRD

の記事が、一番参考になりました。

その他、

も参考に。




 

9. ログの設定

Fabric スクリプト実行後の状態ではアプリケーションの各種ログが出力されないようになっているので、ここで、Mezzanine のログ設定を変更します。


まず、ログを出力するディレクトリを掘っておきます。
なお、gunicorn のプロセスは webapp ユーザで起動されるので、ディレクトリのオーナーを webapp.webapp にしておきます。

$ sudo mkdir /var/log/akiyokoproject
$ sudo chown webapp. /var/log/akiyokoproject


config/local_settings.py の末尾に、以下の設定を追加します。

################
# LOG SETTINGS #
################
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'DEBUG',
            'class': 'logging.FileHandler',
            'filename': '/var/log/akiyokoproject/application.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'DEBUG',
            'propagate': True,
        },
    },
}

(上記は、DEBUGレベル以上のログを「/var/log/akiyokoproject/application.log」に出力する設定になります。)


gunicorn を再起動して設定を反映させます。

$ sudo supervisorctl restart all


 

10. タイムゾーンを Asia/Tokyo に変更

これはちょっと「やっちまった系」です。

本番稼働して何日か経ってからふと思い立ってタイムゾーンを「Asia/Tokyo」に変更したのですが、本来は、Fabric スクリプトを実行する前にやっておくべきだったかもしれません。。

10.1. Ubuntuサーバのタイムゾーンを変更

$ timedatectl
      Local time: Mon 2016-07-25 13:02:02 UTC
  Universal time: Mon 2016-07-25 13:02:02 UTC
        Timezone: Etc/UTC (UTC, +0000)
     NTP enabled: yes
NTP synchronized: no
 RTC in local TZ: no
      DST active: n/a

$ sudo timedatectl set-timezone Asia/Tokyo

$ timedatectl
      Local time: Mon 2016-07-25 22:02:44 JST
  Universal time: Mon 2016-07-25 13:02:44 UTC
        Timezone: Asia/Tokyo (JST, +0900)
     NTP enabled: yes
NTP synchronized: no
 RTC in local TZ: no
      DST active: n/a

$ date
Mon Jul 25 22:05:10 JST 2016

(参考)


 

10.2. settings.py の修正

settings.py の TIME_ZONE を「Asia/Tokyo」に変更して、プロセスを再起動します。

config/settings.py

@@ -101,7 +101,7 @@ ALLOWED_HOSTS = []
 # timezone as the operating system.
 # If running in a Windows environment this must be set to the same as your
 # system time zone.
-TIME_ZONE = 'UTC'
+TIME_ZONE = 'Asia/Tokyo'

 # If you set this to True, Django will use timezone-aware datetimes.
 USE_TZ = True

 

10.3. MySQL のタイムゾーン変更

本番運用を開始した後に Ubuntu のタイムゾーンを変更したからか(十中八九それが原因だと思いますが・・)、Django Admin で [Content] > [Blog posts] に移動しようとすると、「Sorry, an error occurred.」と画面に出て、以下のエラーログが出るようになってしまいました。

    ・
    ・
ValueError: Database returned an invalid value in QuerySet.datetimes(). Are time zone definitions for your database and pytz installed?
Exception while resolving variable 'get_admin_url' in template 'includes/editable_toolbar.html'.
Traceback (most recent call last):
  File "/home/webapp/.virtualenvs/akiyokoproject/local/lib/python2.7/site-packages/django/template/base.py", line 905, in _resolve_lookup
    (bit, current))  # missing attribute
VariableDoesNotExist: Failed lookup for key [get_admin_url] in u'None'

 
先に、MySQL の設定を確認しておきます。

$ mysql -u root -p
mysql> show variables like '%time_zone%';
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | JST    |
| time_zone        | SYSTEM |
+------------------+--------+
2 rows in set (0.00 sec)

mysql> select count(*) from `mysql`.`time_zone_name`;
+----------+
| count(*) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)


対策として、タイムゾーンデータをインポートしました(ちなみに、最後の「mysql」はテーブル名)。 *14

$ mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql
Enter password:
Warning: Unable to load '/usr/share/zoneinfo/iso3166.tab' as time zone. Skipping it.
Warning: Unable to load '/usr/share/zoneinfo/leap-seconds.list' as time zone. Skipping it.
Warning: Unable to load '/usr/share/zoneinfo/zone.tab' as time zone. Skipping it.

### ↓のコマンドを実行すると、MySQLを再起動しなくてもOK
$ mysql -u root -p -e "flush tables;" mysql

(参考)mysql - Database returned an invalid value in QuerySet.dates() - Stack Overflow


なお、「MySQLでタイムゾーンを設定する - Qiita」に書かれているように、「/etc/my.cnf」のタイムゾーンを「default-time-zone = 'Asia/Tokyo'」などと追加しなくてもよさそうです(詳細は不明)。


最後に、MySQL の設定を再び確認。

$ mysql -u root -p
mysql> show variables like '%time_zone%';
+------------------+--------+
| Variable_name    | Value  |
+------------------+--------+
| system_time_zone | JST    |
| time_zone        | SYSTEM |
+------------------+--------+
2 rows in set (0.00 sec)

mysql> select count(*) from `mysql`.`time_zone_name`;
+----------+
| count(*) |
+----------+
|     1808 |
+----------+
1 row in set (0.00 sec)

(「/etc/my.cnf」のタイムゾーンを変更すると、time_zone の値は「Asia/Tokyo」になりますが)time_zone は「SYSTEM」のままです。


これで解決しました!!





 

11. sitemap.xml / robot.txt の設定

これは公開直前におこなう作業ですが、Google Search Console の設定に先駆けて、sitemap.xml および robot.txt の設定をしておきます。


これまたありがたいことに、Mezzanine には、sitemap.xml および robot.txt を提供してくれる仕組みがすでに整っています。


11.1. sitemap.xml の設定

sitemap.xml については、Mezzanine にデフォルトで「django.contrib.sitemaps」がインストールされているので、「/sitemap.xml」にアクセスするとコンテンツに応じた sitemap.xml が自動的に生成されて表示されます。

しかしながら Mezzanine 4.1.0 では、「/sitemap.xml」にアクセスすると以下のエラーが出てしまいます。

Internal Server Error: /sitemap.xml
Traceback (most recent call last):
  File "/home/webapp/.virtualenvs/akiyokoproject/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 149, in get_response
    response = self.process_exception_by_middleware(e, request)
    (中略)
  File "/home/webapp/.virtualenvs/akiyokoproject/local/lib/python2.7/site-packages/mezzanine/core/sitemaps.py", line 27, in items
    return list(Displayable.objects.url_map(in_sitemap=True).values())
  File "/home/webapp/.virtualenvs/akiyokoproject/local/lib/python2.7/site-packages/mezzanine/core/managers.py", line 366, in url_map
    home = self.model(title=_("Home"))
  File "/home/webapp/.virtualenvs/akiyokoproject/local/lib/python2.7/site-packages/django/db/models/base.py", line 416, in __init__
    val = field.get_default()
  File "/home/webapp/.virtualenvs/akiyokoproject/local/lib/python2.7/site-packages/django/db/models/fields/related.py", line 905, in get_default
    if isinstance(field_default, self.remote_field.model):
TypeError: isinstance() arg 2 must be a class, type, or tuple of classes and types
Exception while resolving variable 'get_admin_url' in template 'includes/editable_toolbar.html'.
Traceback (most recent call last):
  File "/home/webapp/.virtualenvs/akiyokoproject/local/lib/python2.7/site-packages/django/template/base.py", line 905, in _resolve_lookup
    (bit, current))  # missing attribute
VariableDoesNotExist: Failed lookup for key [get_admin_url] in u'None'


この不具合は本家で修正されていたのですが、残念ながら、私が本番デプロイした Mezzanine 4.1.0 にはまだマージされていませんでした(4.2.0 から適用されています)。
Fix dummy homepage object in sitemap. Closes #1516. · stephenmcd/mezzanine@94d5729 · GitHub


そこで以下の手順で、Mezzanine 4.1.0 にパッチを充てました。


まず、GitHub 上で stephenmcd/mezzanine を clone しておきます。
その後、Mac 上で以下のコマンドを実行します。

### ローカルに clone
$ git clone https://github.com/akiyoko/mezzanine.git

### tag 4.1.0 でチェックアウト(ブランチ名は「release」)
$ git checkout -b release refs/tags/4.1.0

### パッチを充てる
$ git cherry-pick 94d57294bcc1e934fdd723546be7ea15bb9dcd1a

### リモートリポジトリに push
$ git push origin release


本番環境にて、akiyoko リポジトリから release タグ指定で Mezzanine をインストールし直します。

$ workon akiyokoproject
$ pip install -U git+https://github.com/akiyoko/mezzanine.git@release#egg=Mezzanine

これで解決しました。


(参考)


 

11.2. robot.txt の設定

Fabric スクリプトを実行することで配置される robots.txt は、Google 検索エンジン bot のクローリングを全てのページで拒否するような設定になっているため、本番サイトの公開前に、以下のように「/admin/」以外をクローリング許可するような設定に書き換えます。


static/robots.txt

User-agent: *
Disallow:

となっているのを、

User-agent: *
Disallow: /admin/

Sitemap: https://akiyoko.com/sitemap.xml

に修正します。
(なお、「static/robots.txt」は、.gitignore に「/static」が含まれているため Git管理下にはなりません。)





 

12. デプロイ後の本番環境確認

最後に、デプロイ後の本番環境の各種設定を確認します。


まずは、tree をインストールします。

$ sudo apt-get -y install tree

12.1. ディレクトリ構成

$ find . -name "*.pyc" -exec rm {} \;
$ tree ~/ -L 4
/home/webapp/
├── db_backup
│   └── akiyokoproject_init.dump
├── logs
│   ├── akiyokoproject_error.log
│   ├── akiyokoproject_error_nginx.log
│   └── akiyokoproject_supervisor
└── mezzanine
    ├── akiyokoproject
    │   ├── config
    │   │   ├── __init__.py
    │   │   ├── local_settings.py
    │   │   ├── settings.py
    │   │   ├── urls.py
    │   │   └── wsgi.py
    │   ├── custom
    │   │   ├── __init__.py
    │   │   ├── static
    │   │   ├── templates
    │   │   └── templatetags
    │   ├── deploy
    │   │   ├── crontab.template
    │   │   ├── gunicorn.conf.py.template
    │   │   ├── local_settings.py.template
    │   │   ├── nginx.conf.template
    │   │   └── supervisor.conf.template
    │   ├── fabfile.py
    │   ├── gunicorn.conf.py
    │   ├── gunicorn.pid
    │   ├── gunicorn.sock
    │   ├── __init__.py
    │   ├── last.db
    │   ├── manage.py
    │   ├── requirements.txt
    │   ├── static
    │   │   ├── admin
    │   │   ├── bootstrap
    │   │   ├── CACHE
    │   │   ├── css
    │   │   ├── filebrowser
    │   │   ├── fonts
    │   │   ├── grappelli
    │   │   ├── images
    │   │   ├── img
    │   │   ├── js
    │   │   ├── media
    │   │   ├── mezzanine
    │   │   ├── plugins
    │   │   ├── robots.txt
    │   │   ├── test
    │   │   └── videos
    │   └── templates
    │       ├── base.html
    │       ├── blog
    │       ├── email
    │       ├── errors
    │       ├── generic
    │       ├── includes
    │       ├── index.html
    │       ├── pages
    │       ├── search_results.html
    │       └── twitter
    └── akiyokoproject.tar


 

12.2. 各種ログ

Nginx: /var/log/nginx

$ sudo ls -al /var/log/nginx/
total 8
drwxr-x---  2 www-data adm    4096 Jun 25 07:06 .
drwxrwxr-x 11 root     syslog 4096 Jun 25 07:06 ..
-rw-r--r--  1 root     root      0 Jun 25 07:06 access.log
-rw-r--r--  1 root     root      0 Jun 25 07:06 error.log


Supervisor: /var/log/supervisor

$ ls -al /var/log/supervisor
total 12
drwxr-xr-x  2 root root   4096 Jun 25 07:07 .
drwxrwxr-x 11 root syslog 4096 Jun 25 07:06 ..
-rw-------  1 root root      0 Jun 25 07:07 gunicorn_akiyokoproject-stderr---supervisor-VP9pQh.log
-rw-r--r--  1 root root    760 Jun 25 07:07 supervisord.log


アプリケーションログ: /var/log/akiyokoproject/

$ ls -al /var/log/akiyokoproject/
total 42520
drwxr-xr-x  2 webapp webapp     4096 Aug 11 20:11 .
drwxrwxr-x 12 root   syslog     4096 Oct 25 06:39 ..
-rw-r--r--  1 webapp webapp 43527221 Oct 25 21:30 application.log


エラー系ログ: /home/webapp/logs/

$ ls -al /home/webapp/logs/
total 3808
drwxrwxr-x 2 webapp   webapp    4096 Aug 11 20:11 .
drwxr-xr-x 8 webapp   webapp    4096 Oct 22 14:38 ..
-rw-r--r-- 1 webapp   webapp   23980 Aug 11 20:04 akiyokoproject_error.log
-rw-r--r-- 1 www-data root   3851602 Oct 25 21:58 akiyokoproject_error_nginx.log
-rw-r--r-- 1 root     root      5985 Aug 11 20:04 akiyokoproject_supervisor


 

12.3. Mezzanine の各種設定

config/local_settings.py

from __future__ import unicode_literals

SECRET_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
NEVERCACHE_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
ALLOWED_HOSTS = ['akiyoko.com']

DATABASES = {
    "default": {
        # Ends with "postgresql_psycopg2", "mysql", "sqlite3" or "oracle".
        "ENGINE": "django.db.backends.mysql",
        # DB name or path to database file if using sqlite3.
        "NAME": "akiyokoproject",
        # Not used with sqlite3.
        "USER": "akiyokoproject",
        # Not used with sqlite3.
        "PASSWORD": "akiyokoprojectpass",
        # Set to empty string for localhost. Not used with sqlite3.
        "HOST": "127.0.0.1",
        # Set to empty string for default. Not used with sqlite3.
        "PORT": "",
    }
}

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTOCOL", "https")

CACHE_MIDDLEWARE_SECONDS = 60

CACHE_MIDDLEWARE_KEY_PREFIX = "akiyokoproject"

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
        "LOCATION": "127.0.0.1:11211",
    }
}

SESSION_ENGINE = "django.contrib.sessions.backends.cache"

################
# AWS SETTINGS #
################
#AWS_ACCESS_KEY_ID = "AKIxxxxxxxxxxxxxxxxx"
#AWS_SECRET_ACCESS_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

##################
# EMAIL SETTINGS #
##################
EMAIL_BACKEND = "django_ses.SESBackend"
AWS_SES_REGION_NAME = "us-west-2"
AWS_SES_REGION_ENDPOINT = "email.us-west-2.amazonaws.com"
#EMAIL_HOST_USER = "admin@akiyoko.com"
#EMAIL_USE_TLS = True
#EMAIL_HOST = "email-smtp.us-west-2.amazonaws.com"
#DEFAULT_FROM_EMAIL = SERVER_EMAIL = u"no-reply <no-reply@akiyoko.com>"

################
# LOG SETTINGS #
################
if not DEBUG:
    LOGGING = {
        'version': 1,
        'disable_existing_loggers': False,
        'handlers': {
            'file': {
                'level': 'DEBUG',
                'class': 'logging.FileHandler',
                'filename': '/var/log/akiyokoproject/application.log',
            },
        },
        'loggers': {
            'django': {
                'handlers': ['file'],
                'level': 'DEBUG',
                'propagate': True,
            },
        },
    }


 

12.4. pip でインストールしたパッケージリスト

(akiyokoproject)$ pip list
beautifulsoup4 (4.4.1)
bleach (1.4.3)
chardet (2.3.0)
Django (1.9.7)
django-appconf (1.0.2)
django-compressor (2.0)
django-contrib-comments (1.7.1)
filebrowser-safe (0.4.3)
future (0.15.2)
grappelli-safe (0.4.2)
gunicorn (19.6.0)
html5lib (0.9999999)
Mezzanine (4.1.0)
MySQL-python (1.2.5)
oauthlib (1.1.2)
Pillow (3.3.0)
pip (8.1.2)
psycopg2 (2.6.2)
python-memcached (1.58)
pytz (2016.4)
rcssmin (1.0.6)
requests (2.10.0)
requests-oauthlib (0.6.1)
rjsmin (1.0.12)
setproctitle (1.1.10)
setuptools (24.0.2)
six (1.10.0)
tzlocal (1.2.2)
wheel (0.29.0)

(2016年7月時点のもの)

 

11.5. Supervisor の設定

以下の 2つの Supervisor 設定ファイルは、Fabric スクリプト実行時に自動生成されたものです。


/etc/supervisor/conf.d/akiyokoproject.conf

[program:gunicorn_akiyokoproject]
command=/home/webapp/.virtualenvs/akiyokoproject/bin/gunicorn -c gunicorn.conf.py -p gunicorn.pid config.wsgi:application
directory=/home/webapp/mezzanine/akiyokoproject
user=webapp
autostart=true
stdout_logfile = /home/webapp/logs/akiyokoproject_supervisor
autorestart=true
redirect_stderr=true
environment=LANG="en_US.UTF-8",LC_ALL="en_US.UTF-8",LC_LANG="en_US.UTF-8"


/home/webapp/mezzanine/akiyokoproject/gunicorn.conf.py

from __future__ import unicode_literals
import multiprocessing

bind = "unix:/home/webapp/mezzanine/akiyokoproject/gunicorn.sock"
workers = multiprocessing.cpu_count() * 2 + 1
errorlog = "/home/webapp/logs/akiyokoproject_error.log"
loglevel = "error"
proc_name = "akiyokoproject"


 

11.6. Nginx の設定

$ ls -al /etc/nginx/
total 80
drwxr-xr-x  6 root root 4096 Jul  8 01:03 .
drwxr-xr-x 92 root root 4096 Aug 11 21:59 ..
drwxr-xr-x  2 root root 4096 Jul 10 15:41 conf
drwxr-xr-x  2 root root 4096 Jun  3 00:16 conf.d
  (中略)
-rw-r--r--  1 root root 1601 Mar  5  2014 nginx.conf
  (中略)
drwxr-xr-x  2 root root 4096 Jul  8 00:14 sites-available
drwxr-xr-x  2 root root 4096 Jul  8 00:15 sites-enabled
  (後略)

$ ls -al /etc/nginx/conf.d/
total 8
drwxr-xr-x 2 root root 4096 Jun  2 15:16 .
drwxr-xr-x 6 root root 4096 Jun 18 04:29 ..

$ ls -al /etc/nginx/sites-available/
total 12
drwxr-xr-x 2 root root 4096 Jul  8 00:14 .
drwxr-xr-x 6 root root 4096 Jul  8 01:03 ..
-rw-r--r-- 1 root root 2593 Mar  5  2014 default

$ ls -al /etc/nginx/sites-enabled/
total 12
drwxr-xr-x 2 root   root   4096 Jun 18 04:30 .
drwxr-xr-x 6 root   root   4096 Jun 18 04:29 ..
-rw-rw-r-- 1 webapp webapp 2226 Jun 18 04:30 akiyokoproject.conf
lrwxrwxrwx 1 root   root     34 Jun 18 04:29 default -> /etc/nginx/sites-available/default


/etc/nginx/nginx.conf

user www-data;
worker_processes 4;
pid /run/nginx.pid;

events {
    worker_connections 768;
    # multi_accept on;
}

http {

    ##
    # Basic Settings
    ##

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    # server_tokens off;

    # server_names_hash_bucket_size 64;
    # server_name_in_redirect off;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    ##
    # Logging Settings
    ##

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    ##
    # Gzip Settings
    ##

    gzip on;
    gzip_disable "msie6";

    # gzip_vary on;
    # gzip_proxied any;
    # gzip_comp_level 6;
    # gzip_buffers 16 8k;
    # gzip_http_version 1.1;
    # gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

    ##
    # nginx-naxsi config
    ##
    # Uncomment it if you installed nginx-naxsi
    ##

    #include /etc/nginx/naxsi_core.rules;

    ##
    # nginx-passenger config
    ##
    # Uncomment it if you installed nginx-passenger
    ##

    #passenger_root /usr;
    #passenger_ruby /usr/bin/ruby;

    ##
    # Virtual Host Configs
    ##

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

(後略)


/etc/nginx/sites-available/default

# You may add here your
# server {
#   ...
# }
# statements for each of your virtual hosts to this file

##
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# http://wiki.nginx.org/Pitfalls
# http://wiki.nginx.org/QuickStart
# http://wiki.nginx.org/Configuration
#
# Generally, you will want to move this file somewhere, and start with a clean
# file but keep this around for reference. Or just disable in sites-enabled.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##

server {
    listen 80 default_server;
    listen [::]:80 default_server ipv6only=on;

    root /usr/share/nginx/html;
    index index.html index.htm;

    # Make site accessible from http://localhost/
    server_name localhost;

    location / {
        # First attempt to serve request as file, then
        # as directory, then fall back to displaying a 404.
        try_files $uri $uri/ =404;
        # Uncomment to enable naxsi on this location
        # include /etc/nginx/naxsi.rules
    }

  (中略)
}

(後略)


/etc/nginx/sites-enabled/akiyokoproject.conf

upstream akiyokoproject {
    server unix:/home/webapp/mezzanine/akiyokoproject/gunicorn.sock fail_timeout=0;
}

server {

    listen 80;
     listen 443 ssl;
    server_name akiyoko.com;
    client_max_body_size 10M;
    keepalive_timeout    15;
    error_log /home/webapp/logs/akiyokoproject_error_nginx.log info;

     ssl_certificate      conf/akiyokoproject.crt;
     ssl_certificate_key  conf/akiyokoproject.key;
     ssl_session_cache    shared:SSL:10m;
     ssl_session_timeout  10m;
     ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA;
     ssl_prefer_server_ciphers on;

    # Deny illegal Host headers
    if ($host !~* ^(akiyoko.com)$) {
        return 444;
    }

    location / {
        proxy_redirect      off;
        proxy_set_header    Host                    $host;
        proxy_set_header    X-Real-IP               $remote_addr;
        proxy_set_header    X-Forwarded-For         $proxy_add_x_forwarded_for;
        proxy_set_header    X-Forwarded-Protocol    $scheme;
        proxy_pass          http://akiyokoproject;
    }

    location /static/ {
        root            /home/webapp/mezzanine/akiyokoproject;
        access_log      off;
        log_not_found   off;
        expires 30d;
    }

    location /robots.txt {
        root            /home/webapp/mezzanine/akiyokoproject/static;
        access_log      off;
        log_not_found   off;
    }

    location /favicon.ico {
        root            /home/webapp/mezzanine/akiyokoproject/static/img;
        access_log      off;
        log_not_found   off;
    }

}

 

まとめ

今回、Mezzanine の本番デプロイとして、

  • 本番サーバ(Ubuntu)を起動
  • Fabric スクリプトを実行
  • Fabric スクリプト実行後の各種設定
    • SSL の設定
    • URL の転送設定
    • BASIC認証の設定
    • MySQL のインストール
    • メールの設定
    • ログの設定
    • タイムゾーンを Asia/Tokyo に変更
    • sitemap.xml / robot.txt の設定

を実施した内容を記載しました。

次回は、Mezzanine 本番設定の第四弾として、「その4:Mezzanine の運用設定」について記載します。


 

参考本

CentOS の本ならたくさんあるのに、Ubuntu の本はあまり無いんですよねぇ。
上級者向けではないですが、こんな感じでしょうか。

絶対つまずかない Linuxサーバー構築ガイド(日経BPパソコンベストムック)

絶対つまずかない Linuxサーバー構築ガイド(日経BPパソコンベストムック)


MySQL 本なら。

MySQL徹底入門 第3版 ~5.5新機能対応~

MySQL徹底入門 第3版 ~5.5新機能対応~

  • 作者: 遠藤俊裕,坂井恵,館山聖司,鶴長鎮一,とみたまさひろ,班石悦夫,松信嘉範
  • 出版社/メーカー: 翔泳社
  • 発売日: 2011/08/26
  • メディア: 大型本
  • 購入: 9人 クリック: 82回
  • この商品を含むブログ (9件) を見る

*1:Mezzanine を採用したのは、個人の趣味です。

*2:Fabric スクリプトの実体は、https://github.com/stephenmcd/mezzanine/blob/4.1.0/mezzanine/project_template/fabfile.py です。

*3:参考:PostgreSQLとMySQLはどちらかに明確な優位性がありますか? - QA@IT

*4:Fabric じゃないけど一応

*5:現時点の最新は 4.2.2 ですが、2016年7月当時は 4.1.0 でした。

*6:実際に運用しているドメインは akiyoko.com ではありませんので悪しからず。

*7:「完全な管理者アクセス権限(AdministratorAccess)」ポリシーを付けた EC2インスタンス用 Role です。

*8:ちょっとした裏技(?)ですが、ubuntu ユーザの authorized_keys ファイルをコピーすれば簡単です。

*9:Git 管理下に置きたくない各種設定は、local_settings.py で管理します。

*10:Mezzanine 4.2.0 から修正されたようです。 https://github.com/stephenmcd/mezzanine/commit/898e330a26500d473663f9602165e40d313677d1

*11:「GoGetSSL」からダウンロードした中間証明書に「^M」が入ってたけど、気にしなくて OK!

*12:秘密鍵は GoGetSSL からメールで送られます。

*13:「ec2-prod」ロールを EC2 に付与しているため、「AWS_ACCESS_KEY_ID」および「AWS_SECRET_ACCESS_KEY」が不要になっています。もし IAMロールをインスタンス起動時に付与しない場合は、6つが最低限必要となります。

*14:以下のように Warning がいくつか出ましたが、特に影響なさそうなので無視しました。