akiyoko blog

akiyoko の IT技術系ブログです

Mezzanine の本番設定(その4: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 REST 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=TODO(FIX ME)

# 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/

# Delete old files
find /tmp -name 'media_backup_*.tar.gz' -mtime +10 -exec rm {} \;
find /tmp -name 'mysql_backup_*.dump' -mtime +10 -exec rm {} \;

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

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


(参考)

 

(2017/3/6 追記)

f:id:akiyoko:20170307012941p:plain:w450
「/tmp」以下のゴミファイルが溜まり過ぎて、「/dev/xvda1」がディスクフルになってしまうという不具合が発生してしまったので、

# Delete old files
find /tmp -name 'media_backup_*.tar.gz' -mtime +10 -exec rm {} \;
find /tmp -name 'mysql_backup_*.dump' -mtime +10 -exec rm {} \;

10日前より古いファイルを削除する処理を加えました。

Traceback (most recent call last):
  File "/usr/lib/python2.7/runpy.py", line 162, in _run_module_as_main
    "__main__", fname, loader, pkg_name)
  File "/usr/lib/python2.7/runpy.py", line 72, in _run_code
    exec code in run_globals
  File "/usr/local/lib/python2.7/dist-packages/virtualenvwrapper/hook_loader.py", line 223, in <module>
    main()
  File "/usr/local/lib/python2.7/dist-packages/virtualenvwrapper/hook_loader.py", line 145, in main
    output.close()
IOError: [Errno 28] No space left on device
-bash: cannot create temp file for here-document: No space left on device

$ df
Filesystem     1K-blocks    Used Available Use% Mounted on
udev              502984      12    502972   1% /dev
tmpfs             101636     352    101284   1% /run
/dev/xvda1       8115168 7679972         0 100% /
none                   4       0         4   0% /sys/fs/cgroup
none                5120       0      5120   0% /run/lock
none              508160       0    508160   0% /run/shm
none              102400       0    102400   0% /run/user


なお、CloudWatch のデフォルトではディスク使用量の監視が利用できないようで、ディスク使用量の監視をするには、

あたりの対応が必要そうです。面倒なので一旦保留ということで。。



 

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 の本番設定(その3:Mezzanine の本番デプロイ)〜AWS 環境構築から運用設定まで〜

こんにちは、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.3 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.3 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 がいくつか出ましたが、特に影響なさそうなので無視しました。

Mezzanine の本番設定(その2:Mezzanine テーマのカスタマイズ)〜AWS 環境構築から運用設定まで〜

こんにちは、akiyoko です。

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

akiyoko.hatenablog.jp


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

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

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

今回はその 2本目、「その2:Mezzanine テーマのカスタマイズ」について説明します。


Mezzanine テーマのカスタマイズとして実施した内容としては、

  • Bootstrap テンプレートの選定
  • Bootstrap テンプレートの設定
  • 各種テンプレートファイルの修正

となります。



 

1. Bootstrap テンプレートの選定

Mezzanine は、テンプレートのCSS が Twitter Bootstrap の Version 3 に対応しています。

Twitter Bootstrap integration


Overview — Mezzanine 4.2.3 documentation


そこで、Mezzanine テーマをカスタマイズするための第一歩として、まずは Bootstrap テンプレートの選定を行いました。


結論から書くと、いろいろ探した中で、「WrapBootstrap」というサイトから「The Project - Multipurpose Template」という有料テンプレートを購入することにしました。価格は、1サイト分の利用ライセンスで $16 でした。


The Project - Multipurpose Template
wrapbootstrap.com


f:id:akiyoko:20161018081943p:plain

デモページもあります。
htmlcoder.me




他にも、

f:id:akiyoko:20161018082028p:plain

f:id:akiyoko:20161018082054p:plain

f:id:akiyoko:20161018082121p:plain

f:id:akiyoko:20161018082153p:plain
などを候補としてチェックしていました。


最終的にこの「The Project」テンプレートを採用することにしたのは、

  • 有料テンプレート($4 〜 $49)の中でも比較的安め($16)
  • 人気がある(購入数ランキングでトップ10)
  • テンプレートが多い(コーポレートやポートフォリオ、ブログ、ECサイトなど 30種類近くのテーマで 200ページ以上)
  • ちょくちょく更新されている

といった理由からです。

特に、必須要件である「ブログ」と「ECサイト」以外にも数多くのテンプレートファイルが含まれていて、今後もいろいろな使い勝手があるんじゃないかと思ったのが大きな決め手になりました。あと、使ってる人が多そう(購入者が多い)、サポートがきちんとしてるっぽい(更新頻度が多い)というのも安心ですよね。


なお、「The Project」の現時点の最新のバージョンは Version 1.3 で、Bootstrap 3.3 系と互換性があるとのこと(Bootstrap: Compatible with 3.3.x)。





 

2. Bootstrap テンプレートの設定

購入した Bootstrap テンプレート「The Project」を、Mac 上に構築した PyCharm プロジェクト(あるいは Ubuntu 上に構築した Mezzanine プロジェクト)に実際に当てていきます。


<前提>

  • Mac 上に構築した PyCharm プロジェクト名は「akiyokoproject」
    • プロジェクトのパスは「~/PycharmProjects/akiyokoproject」
  • ダウンロードした「products%2FWB0F82581.zip」をダブルクリックして解凍済み


 

2.1. Bootstrap テンプレートの static ファイルを配置

Mac 上に構築した PyCharm プロジェクトに、Bootstrap テンプレートの static ファイルを配置します。

以下、Mac での手順です。

### Bootstrap の js, css 等の static ファイルをコピーするための前準備
$ cd ~/Downloads/the_project_v.1.3/
### HFS Plus の拡張属性(Extended Attributes)を除去
$ xattr -cr .
$ find . -type d -exec chmod 775 {} \;
$ find . -type f -exec chmod 664 {} \;

### custom ディレクトリを作成
$ cd ~/PycharmProjects/akiyokoproject/
$ mkdir -p custom/{templates,static}
$ touch custom/__init__.py

### 「The Project」テンプレートの html/template 配下のファイルをコピー
$ cp -a ~/Downloads/the_project_v.1.3/html/template/bootstrap custom/static/
$ cp -a ~/Downloads/the_project_v.1.3/html/template/css custom/static/
$ cp -a ~/Downloads/the_project_v.1.3/html/template/fonts custom/static/
$ cp -a ~/Downloads/the_project_v.1.3/html/template/images custom/static/
$ cp -a ~/Downloads/the_project_v.1.3/html/template/js custom/static/
$ cp -a ~/Downloads/the_project_v.1.3/html/template/plugins custom/static/
$ cp -a ~/Downloads/the_project_v.1.3/html/template/videos custom/static/


 

2.2. collecttemplates コマンドを修正

Mezzanin のデフォルトテンプレートを custom アプリケーションにコピーしてくれる便利なコマンドが、Mezzanin には用意されています。

### テンプレートを全コピーしてくれるコマンド
$ python manage.py collecttemplates

(-a オプションを付けると、admin のテンプレートも対象に入れる)

しかしながら、コマンドを実行すると、

  (略)
  File "/home/webapp/.virtualenvs/akiyokoproject/local/lib/python2.7/site-packages/mezzanine/core/management/commands/collecttemplates.py", line 42, in handle
    to_dir = settings.TEMPLATE_DIRS[0]
IndexError: list index out of range

という、Django 1.9 ならではのエラーが出たので(2016年7月時点)、
python manage.py collecttemplates fails with error IndexError: list index out of range · Issue #1512 · stephenmcd/mezzanine · GitHub
に従って暫定対応しました。 *2

diff --git a/config/settings.py b/config/settings.py
index bfd3958..5371af6 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -220,6 +220,7 @@ TEMPLATES = [
         },
     },
 ]
+TEMPLATE_DIRS = [TEMPLATES[0]['DIRS'][0]]

 if DJANGO_VERSION < (1, 9):
     del TEMPLATES[0]["OPTIONS"]["builtins"]


 

2.3. デフォルトテンプレートを templates 配下にコピー

一旦、Mezzanine デフォルトテンプレートを templates/ に全コピーしておきます。

$ python manage.py collecttemplates


 

2.4. custom アプリケーションを INSTALLED_APPS に登録

最後に、custom アプリケーションを settings.py の INSTALLED_APPS に登録します。

$ vi config/settings.py

---
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [
            os.path.join(PROJECT_ROOT, "custom/templates"),
            os.path.join(PROJECT_ROOT, "templates")
        ],
        "APP_DIRS": True,
    ・
    ・

INSTALLED_APPS = (
    ・
    ・
    # "mezzanine.accounts",
    # "mezzanine.mobile",
    "custom",
)
---

ちなみに、「INSTALLED_APPS」を修正しないと、「DEBUG = False」の場合は static ファイルをちゃんと認識してくれるものの、「DEBUG = True」の場合には認識してくれなかったので NG。つまり、「INSTALLED_APPS」への修正も必要だし、「TEMPLATES.DIRS」の修正もテンプレートファイル参照の優先度を付けるために必要となります。


またこの変更により、

$ python manage.py collecttemplates

を実行すると、Mezzanine のデフォルトテンプレートを custom 配下にコピーしてくれるようになります。

なので例えば、以下のコマンドは「cp -a ~/.virtualenvs/akiyokoproject/lib/python2.7/site-packages/mezzanine/core/templates/base.html custom/templates/」と同等になります。

$ python manage.py collecttemplates -t base.html

 

3. 各機能のテンプレートファイル修正

3.1. base.html のヘッダ修正

Bootstrap テンプレートの各種 static ファイルを読み込んでいるのが、base.html です。

なのでまず最初に、base.html の head 部分を
http://bitofpixels.com/blog/mezzatheming-creating-mezzanine-themes-part-1-basehtml/
に沿って修正しました。

diff --git a/custom/templates/base.html b/custom/templates/base.html
index 122e702..9d4149e 100644
--- a/custom/templates/base.html
+++ b/custom/templates/base.html
@@ -15,26 +15,71 @@
 <link rel="alternate" type="application/atom+xml" title="Atom" href="{% url "blog_post_feed" "atom" %}">
 {% endifinstalled %}

+<!-- Web Fonts -->
+<link href='//fonts.googleapis.com/css?family=Roboto:400,300,300italic,400italic,500,500italic,700,700italic' rel='stylesheet' type='text/css'>
+<link href='//fonts.googleapis.com/css?family=Raleway:700,400,300' rel='stylesheet' type='text/css'>
+<link href='//fonts.googleapis.com/css?family=Pacifico' rel='stylesheet' type='text/css'>
+<link href='//fonts.googleapis.com/css?family=PT+Serif' rel='stylesheet' type='text/css'>
+
 {% compress css %}
-<link rel="stylesheet" href="{% static "css/bootstrap.css" %}">
-<link rel="stylesheet" href="{% static "css/mezzanine.css" %}">
-<link rel="stylesheet" href="{% static "css/bootstrap-theme.css" %}">
-{% if LANGUAGE_BIDI %}
-<link rel="stylesheet" href="{% static "css/bootstrap-rtl.css" %}">
-{% endif %}
-{% ifinstalled cartridge.shop %}
-<link rel="stylesheet" href="{% static "css/cartridge.css" %}">
-{% if LANGUAGE_BIDI %}
-<link rel="stylesheet" href="{% static "css/cartridge.rtl.css" %}">
-{% endif %}
-{% endifinstalled %}
+<!-- Bootstrap core CSS -->
+<link href="{% static "bootstrap/css/bootstrap.css" %}" rel="stylesheet">
+<!-- Font Awesome CSS -->
+<link href="{% static "fonts/font-awesome/css/font-awesome.css" %}" rel="stylesheet">
+<!-- Fontello CSS -->
+<link href="{% static "fonts/fontello/css/fontello.css" %}" rel="stylesheet">
+<!-- Plugins -->
+<link href="{% static "plugins/magnific-popup/magnific-popup.css" %}" rel="stylesheet">
+<link href="{% static "plugins/rs-plugin/css/settings.css" %}" rel="stylesheet">
+<link href="{% static "css/animations.css" %}" rel="stylesheet">
+<link href="{% static "plugins/owl-carousel/owl.carousel.css" %}" rel="stylesheet">
+<link href="{% static "plugins/owl-carousel/owl.transitions.css" %}" rel="stylesheet">
+<link href="{% static "plugins/hover/hover-min.css" %}" rel="stylesheet">
+<!-- The Project's core CSS file -->
+<link href="{% static "css/style.css" %}" rel="stylesheet" >
+<!-- The Project's Typography CSS file, includes used fonts -->
+<!-- Used font for body: Roboto -->
+<!-- Used font for headings: Raleway -->
+<link href="{% static "css/typography-default.css" %}" rel="stylesheet" >
+<!-- Color Scheme (In order to change the color scheme, replace the blue.css with the color scheme that you prefer)-->
+<link href="{% static "css/skins/purple.css" %}" rel="stylesheet">
+<!-- Custom css -->
+<link href="{% static "css/custom.css" %}" rel="stylesheet">
 {% block extra_css %}{% endblock %}
 {% endcompress %}

 {% compress js %}
-<script src="{% static "mezzanine/js/"|add:settings.JQUERY_FILENAME %}"></script>
-<script src="{% static "js/bootstrap.js" %}"></script>
-<script src="{% static "js/bootstrap-extras.js" %}"></script>
+<!-- Jquery and Bootstap core js files -->
+<script type="text/javascript" src="{% static "plugins/jquery.min.js" %}"></script>
+<script type="text/javascript" src="{% static "bootstrap/js/bootstrap.min.js" %}"></script>
+<!-- Modernizr javascript -->
+<script type="text/javascript" src="{% static "plugins/modernizr.js" %}"></script>
+<!-- jQuery Revolution Slider  -->
+<script type="text/javascript" src="{% static "plugins/rs-plugin/js/jquery.themepunch.tools.min.js" %}"></script>
+<script type="text/javascript" src="{% static "plugins/rs-plugin/js/jquery.themepunch.revolution.min.js" %}"></script>
+<!-- Isotope javascript -->
+<script type="text/javascript" src="{% static "plugins/isotope/isotope.pkgd.min.js" %}"></script>
+<!-- Magnific Popup javascript -->
+<script type="text/javascript" src="{% static "plugins/magnific-popup/jquery.magnific-popup.min.js" %}"></script>
+<!-- Appear javascript -->
+<script type="text/javascript" src="{% static "plugins/waypoints/jquery.waypoints.min.js" %}"></script>
+<!-- Count To javascript -->
+<script type="text/javascript" src="{% static "plugins/jquery.countTo.js" %}"></script>
+<!-- Parallax javascript -->
+<script src="{% static "plugins/jquery.parallax-1.1.3.js" %}"></script>
+<!-- Contact form -->
+<script src="{% static "plugins/jquery.validate.js" %}"></script>
+<!-- Background Video -->
+<script src="{% static "plugins/vide/jquery.vide.js" %}"></script>
+<!-- Owl carousel javascript -->
+<script type="text/javascript" src="{% static "plugins/owl-carousel/owl.carousel.js" %}"></script>
+<!-- SmoothScroll javascript -->
+<script type="text/javascript" src="{% static "plugins/jquery.browser.js" %}"></script>
+<script type="text/javascript" src="{% static "plugins/SmoothScroll.js" %}"></script>
+<!-- Initialization of Plugins -->
+<script type="text/javascript" src="{% static "js/template.js" %}"></script>
+<!-- Custom Scripts -->
+<script type="text/javascript" src="{% static "js/custom.js" %}"></script>
 {% block extra_js %}{% endblock %}
 {% endcompress %}


なお、
http://bitofpixels.com/blog/mezzatheming-creating-mezzanine-themes-part-1-basehtml/
の「Updating static asset locations」に記載されている通り、staticファイルのパス指定は「{% static %}」で囲まないと、ファイルアクセス時に

UncompressableFileError: 'bootstrap/css/bootstrap.css' isn't accessible via COMPRESS_URL ('/static/') and can't be compressed

というエラーが出てしまうので注意です。




 

3.2. 各機能のテンプレート修正

どの機能がどのテンプレートファイルに対応するかというのを理解するのが、Mezzanine のテンプレートをカスタマイズする上で非常に重要なポイントになります。

日本語の記事では、Monotalk さんのブログが分かりやすいかと思います(日本語の記事はとても少ないので貴重です!)。
mezzanineのテーマを作成してみました。 | Monotalk


以下、(他にも修正が必要なファイルがあるかもしれませんが)私が実際に修正したファイルの一覧です。


トップページ

  • custom/static/img/slide-1.jpg
  • custom/static/img/slide-2.jpg
  • custom/templates/base.html
  • custom/templates/index.html

ドロップダウンメニュー

  • custom/templates/base.html
  • custom/templates/pages/menus/dropdown.html

ブログ

  • custom/static/css/custom.css
  • 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

Twitter 連携

  • custom/static/css/custom.css
  • custom/templates/twitter/tweets.html

  

3.3. 入力フォームの form-control 問題対応

少し細かいですが、Form ページに使用されるテンプレート(custom/templates/includes/form_fields.html)内の入力フィールドタグに、Bootstrap 標準の「form-control」という CSS クラスが無いために、Bootstrap のスタイルがうまく当たらないという問題が発覚しました。


参考サイト


上記のサイトを参考にして、custom 配下に templatetags/__init__.py および templatetags/add_attributes.py を追加し、custom/templates/includes/form_fields.html を修正しました。

custom/
    static/
    templates/
    templatetags/
        __init__.py
        add_attributes.py
    __init__.py


custom/templatetags/add_attributes.py

from django import template
register = template.Library()


@register.filter(name='add_attributes')
def add_attributes(field, css):
    attrs = {}
    definition = css.split(',')

    for d in definition:
        if ':' not in d:
            attrs['class'] = d
        else:
            t, v = d.split(':')
            attrs[t] = v

    return field.as_widget(attrs=attrs)


custom/templates/includes/form_fields.html

diff --git a/custom/templates/includes/form_fields.html b/custom/templates/includes/form_fields.html
index f3d8697..f924e87 100644
--- a/custom/templates/includes/form_fields.html
+++ b/custom/templates/includes/form_fields.html
@@ -1,4 +1,4 @@
-{% load mezzanine_tags %}
+{% load mezzanine_tags add_attributes %}

 {% nevercache %}
 <input type="hidden" name="referrer" value="{{ request.META.HTTP_REFERER }}">
@@ -12,7 +12,7 @@
 <div class="form-group input_{{ field.id_for_label }} {{ field.field.type }}
     {% if field.errors %} has-error{% endif %}">
     {% if field.label %}<label class="control-label" for="{{ field.auto_id }}">{{ field.label }}</label>{% endif %}
-    {{ field }}
+    {{ field|add_attributes:"form-control" }}
     {% if field.errors %}
     <p class="help-block">
         {% for e in field.errors %}

 

まとめ

今回、Mezzanine テーマのカスタマイズとして、

  • Bootstrap テンプレートの選定
  • Bootstrap テンプレートの設定
  • 各種テンプレートファイルの修正

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

次回は、Mezzanine 本番設定の第三弾として、「その3:Mezzanine の本番デプロイ」について記載します。


 

参考本

Twitter Bootstrap 3 関連の日本語の本は、数えるほどしかありません。
もし本格的に勉強したいのであれば、是非。

UIまで手の回らないプログラマのためのBootstrap 3実用ガイド

UIまで手の回らないプログラマのためのBootstrap 3実用ガイド

Bootstrapファーストガイド―CSS設計の手間を大幅に削減!

Bootstrapファーストガイド―CSS設計の手間を大幅に削減!

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

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

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

こんにちは、akiyoko です。

Mezzanine は Python製の WordPress風フルスタックCMSフレームワークですが、個人的にブログサイト(将来的には ECサイトを増設予定)を本番運用するために、昨年12月頃から調査をしてきました。

akiyoko.hatenablog.jp


それ以来いろいろと企画やら調整を地道に進めてきましたが、この 7月にようやく本番運用に漕ぎ着けることができました。

それを記念して(?)AWS の初期設定から Mezzanine の本番デプロイ、ちょっとした運用設定までの記録を一旦ここでまとめておきたいと思います。

かなり長くなるので、

の 4本に記事を分けました。

今回はその 1本目、「その1:AWS 環境構築」について説明します。


AWS 環境構築として実施した内容としては、

  • IAM 設定(ルートアカウント・admin ユーザの設定)
  • VPC 設定(1ネットワーク+ 1サブネット)
  • EC2 設定(Ubuntu 14.04 LTS インスタンスの起動)
  • Amazon SES 設定(ドメイン認証、送信制限の解除申請)

となります。



 

1. IAM の初期設定

基本方針として、AWS ルートアカウントは原則使用せず、AdministratorAccess 権限を持たせた管理者ユーザー「admin」を新たに作成して利用することとします。

なお、ルートアカウントは、MFA(二段階認証)でセキュリティを強化しておきます。


(参考)


 

1. 1. ルートアカウントに MFA 導入

1.1.1. スマホに仮想MFAアプリケーションをインストール

まず、Android に

というアプリをインストールします。


なお、仮想MFA アプリケーションの一覧はこちらです。
私は Android ユーザなので Android を使っていますが、その他のスマホを使っている場合は適宜別のアプリを検討してください。

f:id:akiyoko:20160829014611p:plain

Android Google AuthenticatorAuthy 2 段階認証
iPhone Google Authenticator
Windows Phone Authenticator
Blackberry Google Authenticator

IAM - Multi-factor Authentication より)



(参考)AWS MFAの設定 - Qiita


 

1.1.2. AWS Management Console での設定

[IAM] > [Dashboard] から、[Security Status] > [Activate MFA on your root account] > [Manage MFA] を選択します。

f:id:akiyoko:20160829014934p:plain

[A virtual MFA device](仮想 MFA デバイス)を選択。

f:id:akiyoko:20160829015326p:plain

[Don't show me this dialog again] を選択して次へ。

f:id:akiyoko:20160829015853p:plain


以下の QRコードを 1.1.1. でインストールした「Google 認証システム」アプリでスキャンして、アプリをルートアカウントと紐付けます。

f:id:akiyoko:20160829020107p:plain


これで、AWS ルートアカウントでのログイン時にアプリで払い出されるワンタイムトークンが必要になるため、不正ログインに対するセキュリティが向上します。

 

1.2. パスワードポリシーの変更

[Dashboard] から、[Security Status] > [Manage Password Policy] をクリックします。

f:id:akiyoko:20160829020329p:plain


ここで、パスワードポリシーを

  • パスワードの最小長:8
  • 英大文字、英小文字、数字の組み合わせ
  • ユーザーにパスワードの変更を許可

とします。

有効期間、再利用禁止などのポリシーは今回設定していませんが、ニーズがあり次第、適宜設定することとします。

f:id:akiyoko:20160829020557p:plain


 

1.3. IAMユーザ、IAMグループの管理

 

1.3.1. IAMユーザ作成

管理者ユーザ「admin」を作成します。

f:id:akiyoko:20160829022915p:plain
f:id:akiyoko:20160829022936p:plain

アクセスキーを CSV(credentials.csv)でダウンロードしておきます。
f:id:akiyoko:20160829023157p:plain

User Name,Access Key Id,Secret Access Key
"admin",AKIxxxxxxxxxxxxxxxxx,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


次に、作成したユーザの [Security Credentials](認証情報)タブから、自動作成パスワードを払い出して設定します。

[Manage Password]をクリック。
f:id:akiyoko:20160829023522p:plain

「Assign an auto-generated password」をチェックして、[Apply]をクリック。
f:id:akiyoko:20160829023547p:plain

念のため、パスワードを CSV(admin_pass_credentials.csv)でダウンロードしておきます。
f:id:akiyoko:20160829023702p:plain

User Name,Password,Direct Signin Link
"admin",xxxxxxxxxxxx,https://53xxxxxxxxxx.signin.aws.amazon.com/console


なお、adminユーザには二段階認証は設定しません。 *1


 

1.3.2. IAMグループ作成

adminユーザのための管理者グループ「admin」を作成します。

f:id:akiyoko:20160829023957p:plain

完全な管理者アクセス権限(AdministratorAccess)ポリシーをアタッチします。

f:id:akiyoko:20160829024245p:plain
f:id:akiyoko:20160829024306p:plain

次に、admin グループに admin ユーザを追加します。

作成したグループを選択して、「Add Users to Group」をクリック。
f:id:akiyoko:20160829024412p:plain

追加したいユーザ(ここでは「admin」)を選択して、「Add Users」をクリック。
f:id:akiyoko:20160829024528p:plain
f:id:akiyoko:20160829024556p:plain


 

1.3.3. admin で再ログイン

以下、adminユーザで操作します。

一旦ログアウトし、

https://{AWS Account ID}.signin.aws.amazon.com/console

から、admin ユーザでログインします。なお、「AWS Account ID」は、AWSアカウントに紐づく 12桁の数字で、My Account ページの[Accout Settings]からも確認できます。


ログインしたら、リージョンを「Tokyo」にしておきます。
f:id:akiyoko:20160829024729p:plain


 

2. VPC の初期設定

以前に書いた 「AWS体験ハンズオン ~セキュア&スケーラブルウェブサービス構築~」に参加してきました - akiyoko blog で実現したような、

  • 別アベイラビリティゾーンに Webサーバを冗長構成して、ELB を使ってロードバランシング
  • プライベートなサブネットに RDS を配置(且つ、別アベイラビリティゾーンにレプリケーション)

は行わず、最小限の構成にしています。


今回構築する構成の最終形は、このようになります。
f:id:akiyoko:20160911110053p:plain


(参考)0から始めるAWS入門①:VPC編 - Qiita

 

2.1. VPC の作成

CIDR が「10.0.0.0/16」の VPC を作成します。

[VPC] > [Your VPCs] から、「Create VPC」ボタンをクリック。

f:id:akiyoko:20160830001821p:plain

以下を設定します。

Name tag P1
CIDR block 10.0.0.0/16
Tenancy Default

f:id:akiyoko:20160830001850p:plain


【注意】
このままの設定では、あとでインスタンスを起動して sudo を実行した際に、下記のエラーが出るので(コマンド自体は実行されているっぽい)、DNS Hostnames の設定を「Yes」に変更します。(すでに EC2インスタンスを起動していた場合には、再起動が必要です。)

$ sudo ls
sudo: unable to resolve host ip-10-0-0-80

f:id:akiyoko:20160830001913p:plain
f:id:akiyoko:20160830001930p:plain

(参考)amazon web services - AWS error - sudo: unable to resolve host ip-10-0-xx-xx - Stack Overflow


 

2.2. Subnet の作成

CIDR が「10.0.0.0/24」のサブネットを作成します。

[Subnets] から、「Create Subnet」ボタンをクリック。

f:id:akiyoko:20160830001951p:plain

以下を設定します。

Name tag P1 Public #1
VPC P1
Availability Zone ap-notrtheast-1a
CIDR block 10.0.0.0/24

f:id:akiyoko:20160830002016p:plain


 

2.3. Internet Gateway の作成

インターネットゲートウェイを作成します。

[Internet Gateways] から、「Create Internet Gateway」ボタンをクリック。

f:id:akiyoko:20160830002038p:plain

名前を「P1 Public #1」に設定。
f:id:akiyoko:20160830002101p:plain

作成した Internet Gateway を選択して「Attach to VPC」ボタンをクリックし、作成した VPC を選択して、Internet Gateway を VPC に紐付けます。

f:id:akiyoko:20160830002124p:plain
f:id:akiyoko:20160830002146p:plain

[Route Tables] から、作成した VPC の Route Table を選択して、Routes を編集します。

f:id:akiyoko:20160830002204p:plain

以下を設定します。

Destination 0.0.0.0./0
Target 作成した Internet Gateway を選択

f:id:akiyoko:20160830002223p:plain
f:id:akiyoko:20160830002254p:plain


これで VPC の設定は完了です。


3. EC2 インスタンスの起動

3.1. SSH鍵の作成

まず、SSH鍵を作成します。

[EC2] > [Key Pairs] から、「Create Key Pair」をクリック。

f:id:akiyoko:20160910223728p:plain

キーペア名は、「aws_p1」としました。
f:id:akiyoko:20160910223833p:plain

作成すると、自動的に鍵がダウンロードされます。
f:id:akiyoko:20160910223853p:plain


ダウンロードした SSH鍵を .ssh 以下に配置します。
(以下の操作は Mac を想定しています。)

$ mv ~/Downloads/aws_p1.pem ~/.ssh/
$ chmod 700 ~/.ssh
$ chmod 600 ~/.ssh/aws_p1.pem
$ ls -l@ ~/.ssh/aws_p1.pem
-rw-------@ 1 akiyoko  staff  1696  6  5 17:35 /Users/akiyoko/.ssh/aws_p1.pem
    com.apple.metadata:kMDItemWhereFroms     199
    com.apple.quarantine      68
$ xattr -d com.apple.metadata:kMDItemWhereFroms ~/.ssh/aws_p1.pem
$ xattr -d com.apple.quarantine ~/.ssh/aws_p1.pem
$ ls -l@ ~/.ssh/aws_p1.pem
-rw-------  1 akiyoko  staff  1696  6  5 17:35 /Users/akiyoko/.ssh/aws_p1.pem

ここで、ダウンロードした際によく分かんない attribute(Extended Attributes と言うものらしい)が付いてしまっているので、削除しています。

(参考)EA (Extended Attributes) の消し方


 

3.2. EC2インスタンス用の Security Group 作成

EC2インスタンスに付与する Security Group を作成します。

f:id:akiyoko:20160910223918p:plain

以下を設定します。

Security group name P1 Webserver #1
Description P1 Webserver #1
VPC P1
Type Protocol Port Range Source
SSH TCP 22 My IP
HTTP TCP 80 My IP
HTTPS TCP 443 My IP

f:id:akiyoko:20160910223941p:plain


 

3.3. EC2 インスタンス用の IAM Role の作成

EC2インスタンス作成時に IAM Role を設定して起動するため、先に作成しておきます。

f:id:akiyoko:20160910224102p:plain

IAM Role の名前は「ec2-prod」(本番 EC2インスタンス用の Role という意味)としておきます。
f:id:akiyoko:20160910224124p:plain

「Role Type」は「Amazon EC2」を選択。
f:id:akiyoko:20160910224151p:plain

完全な管理者アクセス権限(AdministratorAccess)ポリシーを選択します。
f:id:akiyoko:20160910224226p:plain

「Create Policy」をクリックして、IAM Role の作成は完了です。
f:id:akiyoko:20160910224252p:plain


 

3.4. EC2インスタンスの起動

EC2インスタンスを起動します。

f:id:akiyoko:20160910224329p:plain

「Ubuntu Server 14.04 LTS」を選択。
f:id:akiyoko:20160910224351p:plain

インスタンスタイプは(必要に応じていつでもスケールアップできるので、最初は)「t2.micro」でよいでしょう。
f:id:akiyoko:20160910224416p:plain

以下を設定します。

Network P1
Subnet P1 Public #1
IAM role ec2-prod
Enable termination protection 「Protect against accidental termination」にチェック

f:id:akiyoko:20160910224442p:plain

f:id:akiyoko:20160910224508p:plain

インスタンス名を「P1 Webserver #1」とします。
f:id:akiyoko:20160910224525p:plain

3.2. で作成した Security Group を設定します。
f:id:akiyoko:20160910224552p:plain

f:id:akiyoko:20160910224616p:plain

3.1. で作成した SSH鍵を設定して、インスタンスを起動します。
f:id:akiyoko:20160910224643p:plain


 

3.5. Elastic IP の紐付け

最後に、起動した EC2インスタンスに Elastic IP を紐付けます。

[Elastic IPs]>[Allocate New Address]をクリック。
f:id:akiyoko:20160910224724p:plain

「Yes, Allocate」をクリックして、Elastic IP を払い出します。
f:id:akiyoko:20160910224751p:plain

払い出した Elastic IP を選択し、[Actions]>[Associate Address]をクリック。
f:id:akiyoko:20160910224815p:plain

3.4. で起動したインスタンスを選択して「Associate」をクリックし、Elastic IP を紐付けます。
f:id:akiyoko:20160910224835p:plain


f:id:akiyoko:20160910224945p:plain


ここで念のため、疎通確認しておきます。

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



 

4. SES の設定

最後に、メールの設定です。
なおここで、AWS 外のレジストラ(ここでは お名前.com)で独自ドメイン「akixxxx.com」を管理していることを前提しています。


まず最初に、Amazon SES は Tokyo リージョンでは対応していないため *2、「Oregon」リージョンに移動します。

f:id:akiyoko:20160910225033p:plain


 

4.1. ドメイン認証

[Domain] > [Verify a New Domain]でドメインを認証します。
f:id:akiyoko:20160910225132p:plain

Domain に「akixxxx.com」を指定します。
f:id:akiyoko:20160910225203p:plain

送信用の TXTレコードと受信用の MXレコードが表示されます。
f:id:akiyoko:20160910225231p:plain


 

4.2. 送信用 TXTレコードと受信用 MXレコード設定

レジストラ(お名前.com)上でドメインNavi にログインし、上記の送信用の TXTレコードと受信用の MXレコードを設定します。


[ドメイン設定] > [ネームサーバーの設定]>[DNS関連機能の設定]から、該当ドメインをチェックして「次へ進む」をクリック。
f:id:akiyoko:20160910225320p:plain

[DNSレコード設定を利用する]>[設定する]をクリック。
f:id:akiyoko:20160910225355p:plain

以下を設定します。

ホスト名 TYPE TTL VALUE 優先 状態
_amazonses.akixxxx.com TXT 3600 (AWS Management Console の Domain Verification Record の値を貼り付け) 有効
akixxxx.com MX 3600 (AWS Management Console の Email Receiving Record の値を貼り付け) 10 有効

なお、MXレコードの優先度は「10」にします(そのまま貼り付けるとダメ。「10」と「inbound-smtp.us-west-2.amazonaws.com」に分割する)。

f:id:akiyoko:20160910225430p:plain

f:id:akiyoko:20160910225513p:plain

f:id:akiyoko:20160910225552p:plain



2時間ほどすると、「Amazon SES Domain Verification SUCCESS」というメールが来ます。
f:id:akiyoko:20160910225630p:plain

ドメインのステータスが「verified」になっていることが確認できます。
f:id:akiyoko:20160910225710p:plain


 

4.3. SES 送信制限の解除申請

ステータスが「verified」になったら、「Request a Sending Limit Increase」の申請をします。

f:id:akiyoko:20160910225743p:plain


申請内容は以下の通り。

Regarding(内容) Service Limit Increase(サービス制限の増加)
Limit Type(制限タイプ) SES Sending Limits(SES送信制限)
Region US West (Oregon)
Limit Desired Daily Sending Quota(希望する一日あたりの送信クォータ)
New limit value 10000
Mail Type(メールの種類) System Notifications(システム通知)
Website URL (メール送信を使用するシステムのURLを指定)
My email-sending complies with the AWS Service Terms and AUP(私は AWS サービス利用規約と AUP に準拠してメールを送信します) Yes
I only send to recipients who have specifically requested my mail(私は明確にリクエストされた受信者にのみメールを送信します) Yes
I have a process to handle bounces and complaints(バウンスや苦情を処理するプロセスがあります) Yes
Use Case Description(申請理由の説明) アカウント登録やパスワード変更など、システムを利用するユーザへの通知に利用します。メール送信は基本的にシステムからの自動送信となり、ユーザ自身が登録したメールアドレス宛に送信するため、バウンスはほとんど発生しません。予想されるユーザ数は当面のところ、○○程度と見込んでいます。(あくまでサンプルですので、状況に合わせて適宜書き直してください。)
Support Language 日本語
Contact method Web(しか選択できません)

f:id:akiyoko:20160910225819p:plain

f:id:akiyoko:20160910225857p:plain


6時間くらい後に承認メールが届きました。
f:id:akiyoko:20160910230541p:plain

Congratulations! After reviewing your case, we have increased your sending quota to 50,000 messages per day and your maximum send rate to 14 messages per second in AWS Region US West (Oregon). Your account has also been moved out of the sandbox, so you no longer need to verify recipient addresses.

AWS Management Console 上はこのようになっています。
f:id:akiyoko:20160910230618p:plain


(参考)Amazon SESによるメール送信環境の構築と実践 | Developers.IO


まとめ

今回、AWS 環境構築として、

  • IAM 設定(ルートアカウント・admin ユーザの設定)
  • VPC 設定(1ネットワーク+ 1サブネット)
  • EC2 設定(Ubuntu 14.04 LTS インスタンスの起動)
  • Amazon SES 設定(ドメイン認証、送信制限の解除申請)

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

次回は、Mezzanine 本番設定の第二弾として、「その2:Mezzanine テーマのカスタマイズ」について記載します。


 

参考本

AWS 関連で比較的新しくて良さげな本を紹介します。

*1:というか、1台のスマホにつき紐付けられるユーザは 1つだけなのかな??

*2:「米国西部(オレゴン)」のほか、「米国東部(バージニア北部)」「欧州(アイルランド)」でも SES を利用できますが、今回は日本から物理的に一番近そうな米国西部を選択しました。(参考)https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/regions.html 東京リージョンに対応しました。(参考)Amazon SES 東京リージョン対応のお知らせ | Amazon Web Services ブログ

GitHub の Wiki に画像を貼り付ける一番簡単な方法(Wiki リポジトリを clone しないバージョン)

GitHub の Wiki に画像を貼り付けようとして画像をドラッグ&ドロップすると、
f:id:akiyoko:20160830005915p:plain
こうなって、画像を貼り付けできませんよね?


「じゃあ、どうやって GitHub Wiki に画像を貼り付けるの?」とググると、たいてい、

GitHub の Wiki をローカルに clone して、
貼り付けたい画像ファイルを add して、commit して push してから、

![Link Text](https://github.com/akiyoko/foo/wiki/images/bar.png "Title")

という形式で Markdown を記述


などというやり方が出てきますが、何だかよく分からないし面倒臭そうですよね(実際、面倒臭いです・・)。


実は、それよりもずっと簡単な方法で GitHub Wiki に画像を貼り付ける方法があるのです。


今回はその方法を説明します。


 

方法

1. GitHub Issue を開く

まず、適当な GitHub Issue を開きます。

f:id:akiyoko:20160830004006p:plain

2. Issue に貼り付けたい画像をドラッグ&ドロップ

開いた Issue に、貼り付けたい画像をドラッグ&ドロップすると・・

f:id:akiyoko:20160830004320p:plain

画像を貼り付けるためのタグが生成されます。

f:id:akiyoko:20160830004515p:plain

3. GitHub Wiki に貼り付ける

2. で生成されたタグを GitHub Wiki にコピペすれば OK です。

f:id:akiyoko:20160830004704p:plain
f:id:akiyoko:20160830004729p:plain

簡単ですね!


 

まとめ

GitHub の Wiki に画像を貼り付けるには、GitHub Issue に画像をドラッグ&ドロップして生成されたタグをコピペするだけでオッケーです。


それにしても、どうして GitHub の Wiki はドラッグ&ドロップで画像をアップロードできないのでしょうね??


Git 関連本

Gitが、おもしろいほどわかる基本の使い方33〈バージョン管理、SourceTree、Bitbucket〉

Gitが、おもしろいほどわかる基本の使い方33〈バージョン管理、SourceTree、Bitbucket〉

Gitポケットリファレンス

Gitポケットリファレンス

【PayPal 決済まとめ】PayPal の決済サービスが分かりにくいので 画面遷移パターンごとに使える決済サービス・API を整理してみた

タイトル通りなのですが、ドキュメントが古かったりとっ散らかっていたりするためか、数多くある PayPal の決済サービスのどれが使えてどれが使えないかが分かりにくかったので整理してみました。特に、日本では使えない決済サービスもあったりするので、それを明確にしたかったというのが今回の動機です。そして今回は、単に決済サービスの一覧を列挙するだけでは面白くないので、画面遷移のユースケース(パターン)ごとに使える決済サービス・API を整理をしてみることにしました。


まず、PayPal の API 体系について簡単に説明すると、データ伝搬方式として NVP(Name-Value Pair)形式あるいは SOAP形式が利用できる「NVP/SOAP APIs*1 という API が古くからあり、まだまだ現役で使われています 。それに対して、2013年頃から新しい「Payments REST API」が提供され、最近では「Braintree v.zero」という最新の API が整備されつつあります。「Braintree v.zero」についてはまだサンドボックスとなっていますが、現時点での日本での対応状況も含めてできるだけ最新の情報をまとめていこうと思います。


なお今回は、通常の Webサイトでの決済(モバイル決済やアプリ内課金ではなく)についてのみまとめてあります。



 

1. 自サイト完結型

PayPal サイトを経由しない唯一のパターンです。
決済のバックエンドプロセスを PayPal が担ってくれます。

「Direct Credit Card Payments」という決済サービスを使用することでこの画面遷移パターンが実現できるのですが、残念ながら日本では利用不可。

画面遷移パターン

f:id:akiyoko:20160822012151p:plain
Website Payments Pro Integration Guide - PayPal Developer の図を元に作成)

① ショッピングカート画面(「購入手続きに進む」ボタン)
    ↓
② 支払い方法選択画面 *2
    ↓
③ 最終確認画面(「今すぐ支払う」ボタン)
    ↓
④ 決済完了画面

 

利用可能な決済サービス・API
決済サービス名 API 利用条件 特記事項
Direct Credit Card Payments NVP/SOAP APIs UKのみ *3 。Website Payments Pro (WPP) 契約が必要 DoDirectPayment
Direct Credit Card Payments Payments REST API 同上

 

メリット
  • PayPal アカウントが無くてもクレジットカードが利用可能(購入者が PayPal を意識することはない)
  • 自サイト内で決済処理が完結するため、離脱が少なくなる
デメリット
  • 日本では利用不可。現在利用できるのが UK のみに限定されていて、近いうちに日本で使えるようになる可能性はかなり低いと思われる



 

2. iframe による埋め込み型

一見すると、「1. 自サイト内完結型」のように PayPal サイトを経由しないように見えるパターン。iframe を使って PayPal サイトの画面を自サイトに埋め込むことで、この画面遷移パターンを実現できます。

iframe で PayPal サイトを埋め込むには、「ウェブペイメントプラス」という有料サービスの契約が必要。

画面遷移パターン

f:id:akiyoko:20160822012210p:plain
https://www.paypalobjects.com/webstatic/en_GB/developer/docs/pdf/hostedsolution_uk.pdf P.12「How Hosted Solution Works」の図を元に作成。緑枠:自サイト、青枠:PayPal サイト)

① ショッピングカート画面(「購入手続きに進む」ボタン)
    ↓
② (iframe の PayPal サイト内)支払い方法選択画面(「今すぐ支払う」ボタン)  *4
    ↓
③ (iframe の PayPal サイト内)決済完了画面

 

利用可能な決済サービス・API
決済サービス名 API 利用条件 特記事項
ウェブペイメントプラス 日本企業, JPY 利用可。ウェブペイメントプラス契約が必要 「ウェブペイメントプラス」は US などでは提供していないサービス。UK など一部の地域で提供されている「Website Payments Pro Hosted Solution」に相当する

 

メリット
  • iframe内で決済処理がされる(見た目上リダイレクトされない)ため、離脱防止になる
  • ウェブペイメントプラスを利用した場合、自サイトと違和感の無いようにカスタマイズした決済画面を PayPal 側にホスティングさせることができる *5。これにより購入者の不安を軽減することができる
  • PayPal アカウントが無くてもクレジットカードが利用可能(購入者が PayPal を意識することはない)
デメリット
  • ウェブペイメントプラスは、基本手数料のほかに月額手数料 3,000円 が別途かかる。*6


 

3. PayPal サイトへのリダイレクト + 通知型

リダイレクト先の PayPal サイト内で決済を完了させ、自サイト側のリスナーで PayPal からの通知(IPN or PDT)を受け取るのがポイントとなる画面遷移パターン。

いわゆる「ウェブペイメントスタンダード(PayPal Payments Standard)」を使う方法と言えばこのパターンになります。

画面遷移パターン

f:id:akiyoko:20160822182429p:plain
https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNIntro/ の図を元に作成。緑枠:自サイト、青枠:PayPal サイト)

① ショッピングカート画面(「PayPal で支払う」ボタン)
    ↓
  (リダイレクト)
    ↓
② (PayPal サイト内)ログイン画面(「ログイン」ボタン)
    ↓
③ (PayPal サイト内)最終確認画面(「今すぐ支払う」ボタン)
    ↓
④ (PayPal サイト内)決済完了画面(「ショッピングサイトに戻る」ボタン)
    ↓
  (リダイレクト)
    ↓
⑤ 決済完了通知を受け取る


ちなみに ②の画面では、購入者が PayPal アカウントを持っていなくても、オプションのクレジットカード情報の入力フォームからそのまま決済処理をそのまま進めることもできます。また、クレジットカード情報の入力フォームを PayPal ログインに優先させてデフォルト表示することもできるようです。

 

利用可能な決済サービス・API
決済サービス名 API 利用条件 特記事項
Payment Buttons + Notification PayPal Button Creation ToolInstant Payment Notification (or Payment Data Transfer) 日本企業, JPY 利用可 PayPal Button Creation Tool は、PayPay サイト上で事前に作成した PayPal ボタンの HTMLコードをコピー&ペーストして使うことができる、「PayPal Payments Standard」の一機能
Payment Buttons + Notification Button Manager APIInstant Payment Notification (or Payment Data Transfer) 日本企業, JPY 利用可 API の種類としては、いずれも NVP/SOAP APIs に分類される

 
なお、通知の種類である Instant Payment Notification (IPN) と Payment Data Transfer (PDT) の違いは、IPN が非同期通知、PDT が同期通知となる点。また、IPN は何度も通知されるのに対し、PDT は通知が一度きりで Confirmation のタイミングだけというデメリットがあるので、PayPal としては IPN を使うことを推奨してます。

(参考)

 

メリット
  • 実装が簡単(後述)
  • 「支払いボタン」作成ツールを使えば、生成された HTMLタグをコピー&ペーストするだけで、Payment Buttons の機能を利用可能
デメリット
  • PayPal サイトにリダイレクトされるので、ある程度の離脱が発生することを許容しないといけない
  • 「実装が簡単」という触れ込みだが、複数サービスを組み合わせる必要があるのと、通知が受け取れなかった場合の異常系を実装するのが手間になる
  • セキュリティ的にあまりよろしくないらしい(*7)が、暗号化ボタンを使えば大丈夫なはず??


 

4. PayPal サイトへのリダイレクト型

リダイレクト先の PayPal サイト内で決済を完了させず、自サイト側の最終確認画面で決裁を完了させる画面遷移パターン。

画面遷移パターン

f:id:akiyoko:20160822182454p:plain
Express Checkout with In-Context Integration Guide - PayPal Developer の図を元に作成。緑枠:自サイト、青枠:PayPal サイト)

① ショッピングカート画面(「PayPal で支払う」ボタン)
    ↓
  (リダイレクト)
    ↓
② (PayPal サイト内)ログイン画面(「ログイン」ボタン)
    ↓
③ (PayPal サイト内)支払承認画面(「支払いに同意」ボタン)
    ↓
  (リダイレクト)
    ↓
④ 最終確認画面(「今すぐ支払う」ボタン)
    ↓
⑤ 決済完了画面

 

利用可能な決済サービス・API
決済サービス名 API 利用条件 特記事項
Express Checkout NVP/SOAP APIs 日本での利用可 *8 SetExpressCheckout, GetExpressCheckoutDetails, DoExpressCheckoutPayment を使う
Stored Credit Card Payments / PayPal Account Payments Payments REST API 日本での利用可 登録済みのクレジットカードで支払う場合は Stored Credit Card Payments、PayPalアカウントで支払う場合は PayPal Account Payments を使う

 

メリット
  • Express Checkout は利用できる国・地域が多い
  • Express Checkout は事例やドキュメントが比較的豊富
デメリット
  • PayPal サイトにリダイレクトされるので、ある程度の離脱が発生することを許容しないといけない


なお、Express Checkout 利用時のリダイレクトURLのパラメータを変更することで、「3. PayPal サイトへのリダイレクト + 通知型」と同じように、PayPal サイト上で決裁を完了させることも可能です。

(参考)Customizing Express Checkout - PayPal Developer




 

5. ポップアップウィンドウ型(In-context Window)

小さなポップアップを立ち上げ、その内部で PayPal サイトを表示させる、新しいタイプの画面遷移パターン。

画面遷移パターン

f:id:akiyoko:20160816235832p:plain
NVP/SOAP Integration - PayPal Developer の図を元に作成。緑枠:自サイト、青枠:PayPal サイト)

① ショッピングカート画面(「PayPal で支払う」ボタン)
    ↓
② (In-context ウィンドウ内)ログイン画面(「ログイン」ボタン)
    ↓
③ (In-context ウィンドウ内)支払承認画面(「支払いに同意」ボタン)
    ↓
④ 最終確認画面(「今すぐ支払う」ボタン)
    ↓
⑤ 決済完了画面

 

利用可能な決済サービス・API
決済サービス名 API 利用条件 特記事項
Braintree v.zero SDK 日本での利用不可 サンドボックスで検証
In-context Express Checkout NVP/SOAP APIs 日本企業, JPY 利用不可 *9

 

メリット
  • ポップアップウィンドウ内で決済処理がされるため、離脱防止になる
デメリット
  • Braintree は、比較的新しいサービスで日本ではまだ利用不可(将来的に使えるようになるか不明だが、いろんな決済手段を透過的に利用できるので期待大)


表中に示したように、NVP/SOAP APIs の利用条件としては公式ドキュメントでは「日本企業, JPY 利用不可」となっていますが、PayPal の中の人に聞いたところもうすでに利用可能だと言っていました。

akiyoko.hatenablog.jp



 

まとめ

販売先のメインが日本ということであれば WebPay の API を使うのが一番簡単らしいのですが、越境EC だと利用規約的に NG らしいので、PayPal を使わざるを得ないという状況がまだまだあるように思います。

そしていざ PayPal を使うとして最初につまずくのが、数多くある PayPal のうちどの決済サービス・API を使えばいいのか全然分からなくてお手上げになるという問題です。
現在日本で実現できる画面遷移パターンは、「1. 自サイト内完結型」を除く、

  • 2. iframe による埋め込み型
  • 3. PayPal サイトへのリダイレクト + 通知型
  • 4. PayPal サイトへのリダイレクト型
  • 5. ポップアップウィンドウ型(In-context Window)

ですので、その中から実現したい画面遷移パターンに合わせて、決済サービス・API を選択すれば良いということになります。


なお、機能ごとに使える地域と通貨のまとめが開発者用のページにまとまっていて、こちらのページは必見です。
https://developer.paypal.com/docs/classic/howto_product_matrix/



また、期待大の最新API の Braintree についてですが、今年の 3〜4月あたりに Sandbox を使ってみたときにはまだまだ使い物にはならなかったという印象でしたが、6月に PayPal の中の人に確認したところ、近い将来使えるようになるということでした。こちらについては今後要検証です。


このまとめ記事が、何かの手助けになれば幸いです。
ただし、自分でも試していないサービスが多いので、間違っている情報がありましたら教えていただけると助かります。


*1:少し古いドキュメントでは「Classic API」と呼ばれるていたのですが、最新の開発者用ドキュメントでは「NVP/SOAP APIs」という名称に統一されたようです。開発者用ドキュメント上ではまだ結構「Classic API」という名称が混在しているようです。。

*2:支払い方法として「PayPal」を選択させることもできるが、今回は省略

*3:https://developer.paypal.com/docs/classic/api/#website-payments-pro

*4:支払い方法として「PayPal」を選択させることもできるが、今回は省略

*5:iframe で埋め込まずにそちらにリダイレクトすることも可能

*6:PayPal(ペイパル) - かんたん&安全なオンライン決済サービス

*7:PayPalのWeb Paymetn StandardとExpress Checkoutの違いが理解できないあなたへ | 高橋文樹.com | プログラミング

*8:https://developer.paypal.com/docs/integration/direct/rest-api-payment-country-currency-support/

*9:https://developer.paypal.com/docs/classic/express-checkout/in-context/#features-not-supported-by-in-context-express-checkout

「何となくJavaScriptを書いていた人が一歩先に進むための本」と「JavaScriptの理解を深めた人がさらにもう一歩先に進むための本」の二冊を読んでトドメに「Effective JavaScript」を読んだら長年のモヤモヤがスッキリして JavaScript 中級者にステップアップできた件

これまでずっとサーバサイドをメインでやってきたとは言え、JavaScript に触れる機会も少なくなかったのですが、正直なところ何度やってもコツが掴めないというか、「JavaScript って独特な言語だなあ」というモヤモヤとした苦手意識がありました。


少し前に、「何となくJavaScriptを書いていた人が一歩先に進むための本」(以下「一歩先」)という本をたまたま Kindle Store で見かけたのをきっかけに、同じ著者の続編「JavaScriptの理解を深めた人がさらにもう一歩先に進むための本」(以下「もう一歩先」)を続けて読み、さらに積ん読してあった「Effective JavaScript JavaScriptを使うときに知っておきたい68の冴えたやり方」と合わせて、三冊の JavaScript 本を一気に読んでみたら、これまでのモヤモヤがかなりスッキリして何だか JavaScript の初心者を卒業できたような気がしたので、忘れないうちにメモを残しておきたいと思います。



「一歩先」「もう一歩先」の二冊は薄くてサクサク読めるので非常にオススメです。それぞれ値段も安いですし。
実戦で今すぐ使える知識が多く載っているのが「一歩先」、ES2015 と JavaScript で一番やっかい(?)な this について詳しく学びたければ「もう一歩先」というイメージでしょうか。


こちらは、JavaScript 中級者になるためのポイントが詰まっている本。気になるところだけ読んでもいいし、最初から読み進めて「難しいな」と感じた時点でストップしてもいい。項目ごとに「憶えておくべきポイント」としてまとめが書かれているのが素晴らしいです。
なおこの本では、ES2015 については触れられていません(ES5 を前提)。



目次


 

ES5 と ES2015(ES6)の対応状況

ES5(ECMAScript 第5版)の主要ブラウザの対応状況は、
ECMAScript 5 compatibility table
となっており、モダンブラウザであればほぼ確実に動くと見てよい。


一方、次期バージョンの ES2015(通称 ES6)は、Chrome, Firefox, Edge 以外はまだまだ未対応のものが多く、Android, iOS も壊滅的。
ECMAScript 6 compatibility table

少なくとも現時点ではES2015を「そのまま」現場に持ち込むことは現実的とは言えないでしょう。


http://analogic.jp/es2015_introduction/

という昨年10月頃からあまり変わっていない状況。

そんな ES2015 を実践投入するには、「Babel」などのトランスパイラ(ES2015 で書かれたコードを ES5 のコードへ変換する仕組み)を使う。


 

プリミティブ型

JavaScript には、

  • boolean
  • number
  • string
  • null
  • undefined

のプリミティブ型が存在し、通常は「値そのもの」を示す。

number, string, boolean にはラッパーオブジェクトが存在し、場合によっては一時的にオブジェクトとして振る舞うことがあるが、null, undefined は常にプリミティブ型となる。

var n = 1;
console.log(n.toString());  // "1"


 

関数

JavaScript では、次のいずれかの方法で関数を定義する。

  • ① 関数宣言(function 命令)
  • ② 関数式(関数リテラル)
  • ③ Function コンストラクタ(ぶっちゃけ使う必要なし!)
① 関数宣言(function 命令)
function add(x, y) {
    return x + y;
}

この形式で定義された関数は、コード実行時にスコープの先頭まで「関数の巻き上げ」(ホイスティング)が発生する。

② 関数式(関数リテラル)
var add = function(x, y) {
    return x + y;
};

この形式で定義された関数は「関数の巻き上げ」が発生しない。

また、以下のような「名前付き関数式」は、可搬性がないので使わないこと。

var add = function hoge(x, y) {
    return x + y;
};

(参考)【JavaScript】関数定義いろいろ - Qiita


 

高階関数

JavaScript は第一級関数で、高階関数を扱える。

高階関数とは「関数自身を引数や戻り値として扱う関数」(Effective JavaScript では「ほかの関数を引数として受け取るか、結果として関数を返す関数」)。

関数もオブジェクトの一種。

function add(x, y) {
    return x + y;
}

// これが高階関数
function calc(func, x, y) {
  return func(x, y);
}

console.log(calc(add, 1, 2));  // 3

(参考)JavaScriptで関数型プログラミングの入門 - Qiita


 

即時関数

() 演算子を使って、関数を即時実行できる。一度しか実行したくないときに。

(function add(x, y) {
    console.log(x + y);
})(1, 2);  // 3


無名関数を即時実行することで、グローバルオブジェクト(の名前空間)を汚染しないようにすることができる。

(function () {
    var name = 'not global';
})();


また、即時関数を応用すれば、ES5 には存在しない「ブロックスコープ」を擬似的に実現できる。*1

まずダメな例。

var arr = [1, 2, 3],
    results = [],
    i;
for (i = 0; i < arr.length; i++) {
    results[i] = function() { return arr[i]; };
}

results[0]();  // undefined
results[1]();  // undefined
results[2]();  // undefined

8〜10行目の results[0]();, results[1]();, results[2](); が実行されるタイミングではグローバル変数 i の値が 3 となっていて、結果が undefined になってしまっている。
即時関数を使ってブロックスコープの変数 j を作成して修正したものが以下。

var arr = [1, 2, 3],
    results = [],
    i;
for (i = 0; i < arr.length; i++) {
    (function() {
        var j = i;
        results[i] = function() { return arr[j]; };
    })();
}

results[0]();  // 1
results[1]();  // 2
results[2]();  // 3


 

引数

JavaScript の関数は、シグネチャ(引数の型と数)を持たないため、引数の型や数のチェックを行わないし、「オーバーロード」という概念も存在しない。


arguments は呼び出し元から渡された引数を管理しているオブジェクトで、暗黙的に生成される。

var checkArgs = function() {
    console.log(arguments[0], arguments[1]);
};

checkArgs(1, 2);  // 1 2
checkArgs(1);  // 1 undefined
checkArgs();  // undefined undefined


 

new 演算子

「一歩先」の

new は、「オブジェクトのインスタンスを返せ!」とコンストラクタに命令するための演算子。

という説明がすごく分かりやすい。

通常、JavaScriptの関数は return文が明示的に指定されていない場合には呼び出し元に undefined が返される。new 演算子を用いて呼び出された場合は、例外的に、return文の有無に関わらずオブジェクトのインスタンスが返却される。

// コンストラクタ関数
var Person = function(name) {
    this.name = name;
}

// prototype プロパティを使って、オブジェクトにメソッドを追加
Person.prototype.hello = function() {
    console.log('I am ' + this.name);
}

// new 演算子によって、person.__proto__ に Person.prototype が代入される
var person = new Person('akiyoko');


 

prototype

ES6 で「クラス」の概念が導入されたが、それ以前には JavaScript には「クラス」は存在しない。prototype がクラスの代わりとして使われる。

(いわゆる「クラス」が実現するような)オブジェクトにメンバ(主にメソッド)を追加する仕組みとして、JavaScript では「prototype プロパティ」を使う。

インスタンス化されたオブジェクトは、使用したコンストラクタの prototype プロパティに対して暗黙の参照を持ち、prototype プロパティに追加されたメソッドに対しても同様に暗黙の参照を持つ。

その際、インスタンス化したオブジェクトは、コンストラクタで定義されたメンバ分のメモリを都度確保するが、prototypeプロパティに格納したメソッド分のメモリは確保しないため、メモリ使用量の節約を目的として prototype プロパティにメソッドを追加する。

つまり、

var Person = function(name) {
    this.name = name;
    this.hello = function() {
        console.log('I am ' + this.name);
    }
}

とするよりも、

var Person = function(name) {
    this.name = name;
}
Person.prototype.hello = function() {
    console.log('I am ' + this.name);
}

とした方がメモリの節約になるので、通常は後者のように書く。

メソッドは必ず prototype プロパティで管理するように徹底する

 

プロトタイプチェーン

プロトタイプチェーンは、JavaScript で「オブジェクト指向の継承」を実現するための仕組み。継承したいオブジェクトのインスタンスを、自身の prototype プロパティとして格納する。(なお継承は、ES5準拠の Object.create でも実現可能。)

var Person = function(name) {
    this.name = name;
}
Person.prototype.hello = function() {
    console.log('I am ' + this.name);
}

var Student = function(name) {
    this.name = name;
}
Student.prototype = new Person();
Student.prototype.study = function() {
    console.log('I am studying now!');
}

ここで、

var akiyoko = new Student('akiyoko');
akiyoko.hello();  // I am akiyoko

を実行する際に、

1)Student オブジェクトの hello メソッドを検索するも、存在せず
   ↓
2)Student オブジェクトが暗黙の参照を持つ Student.prototype(すなわち Person オブジェクト)の hello メソッドを検索するも、存在せず
   ↓
3)Person オブジェクトが暗黙の参照を持つ Person.prototype の hello メソッドを検索して見つかったので、実行!


というプロトタイプチェーンを辿る。


 

this

this が結合する値(レシーバ)は、メソッドおよび関数の呼び出し時に決まる(呼び出され方によって決まる)。

① メソッド呼び出しされた場合

メソッド呼び出しされた場合のメソッド内の this は、メソッドプロパティがルックアップされるオブジェクトに結合される。というわけで多くの場合、this は呼び出し元のオブジェクトを指すことになる。

var akiyoko = {
    name : 'akiyoko',
    hello : function() {
        console.log('I am ' + this.name);  // ★
    }
}

// メソッド内の this(★)は、メソッドプロパティがルックアップされる akiyoko オブジェクトに結合される
akiyoko.hello();  // I am akiyoko

上の例では、hello プロパティがルックアップされた akiyoko オブジェクトを this のレシーバとして、hello が呼び出される。

 

② new 演算子を使ってインスタンス化された場合

new 演算子を使ってインスタンス化された場合のコンストラクタ内の this は、そのインスタンス自身に結合される。

var Person = function(name) {
    this.name = name;  // ★
    this.hello = function() {  // ★
        console.log('I am ' + this.name);
    }
}

// コンストラクタ内の this(★)は、返却されるインスタンス自身に結合される
var akiyoko = new Person('akiyoko');

 

③ 関数呼び出しされた場合

関数呼び出しされた場合の関数内の this は、グローバルオブジェクトに結合される。

var name = 'global';

var hello = function() {
    console.log('I am ' + this.name);  // この this はグローバルオブジェクトに結合される
}

hello();  // I am global

 

④ メソッドおよび関数が高階関数の引数として渡された場合

メソッドおよび関数が Array.prototype.forEach() のような高階関数の引数として渡された場合は、少しやっかい。


以下の例では、akiyoko.hello のレシーバは akiyoko にならない。akiyoko.hello がいつどういった形で呼び出されるのかはその外側のメソッドおよび関数(ここでは forEach *2)の 実装次第 である。

var Person = function(name) {
    this.name = name;
    this.hello = function() {
        console.log('I am ' + this.name);
    }
}

var akiyoko = new Person('akiyoko');
[1, 2].forEach(akiyoko.hello);  // I am I am

レシーバを akiyoko にするためには、bind を使って this を束縛すればよい。 *3

var Person = function(name) {
    this.name = name;
    this.hello = function() {
        console.log('I am ' + this.name);
    }
}

var akiyoko = new Person('akiyoko');
[1, 2].forEach(akiyoko.hello.bind(akiyoko));  // I am akiyoko I am akiyoko

別の解決策として、this というキーワードを使わなければよいということで、self, _this, that といった名前の変数に this の参照を「逃がしてやる」方法もある。


次は、イベントリスナーの例。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>JavaScript Test | akiyoko blog</title>
</head>
<body>
<button type="submit" id="submit" name="action" value="send">送信</button>
<script>
    var el = document.querySelector('#submit');
    el.addEventListener('click', function () {
        console.log('this=', this);  // ★
    }, false);
</script>
</body>
</html>

上の例では、addEventListener の引数で渡される無名関数内の this(★)は発火元の DOM要素を指すため、ボタン押下時の実行結果は以下のようになる。

this= <button type=<200b>"submit" id=<200b>"submit" name=<200b>"action" value=<200b>"send"><200b>送信<200b></button><200b>

以下の参考サイトがさらに詳しい。

ハンドラ内でのthisの値について


イベントハンドラの関数内から、その発火元の要素を参照したくなる事がよくあります。 addEventListener()を使用して、関数を割り当てたのであれば、 呼び出し元への参照がthisの値となって関数内に渡されます。


(中略)比較として、仮にHTML内にハンドラが次のように配置されていた場合、

<table id="t" onclick="modifyText();">

onclickイベントで呼び出された際の、modifyText()内のthisの値は、 グローバル(window)オブジェクトへの参照になります。


.addEventListener() | JavaScript 日本語リファレンス | js STUDIO


 

クロージャ

クロージャとは、分かりやすく言うと、

「ローカル変数を参照している、関数の中に定義された関数」

である。

下の例では、

ローカル変数(count)を参照している、関数(outerFunc)の中に定義された関数(innerFunc)

がクロージャ。

function outerFunc(initCount) {
    var count = initCount;

    var innerFunc = function() {
        return ++count;
    };
    return innerFunc;
}

var myCounter = outerFunc(100);
console.log(myCounter());  // 101
console.log(myCounter());  // 102
console.log(myCounter());  // 103

「innerFunc の Callオブジェクト ⇒ outerFunc の Callオブジェクト ⇒ グローバルオブジェクト」というスコープチェーンが生成され、 innerFunc が有効である限りそのスコープチェーンは保持される。innerFunc関数は、myCounter変数に格納されるため、outerFunc関数が呼び出し終わっても破棄されず、従って当該スコープチェーンも破棄されない。これにより、Callオブジェクトに管理されているローカル変数(count)も破棄されず、代入された値が残ったままとなる。


(参考)JavaScript のスコープチェーンとクロージャを理解する - tacamy.blog

*1:ES2015 では let, const を使ってブロックスコープを実現可能。http://qiita.com/tuno-tky/items/74ca595a9232bcbcd727

*2:ちなみに forEach の場合は this はグローバルオブジェクトを指す。

*3:call メソッドや apply メソッドでも this を束縛することができるが、レシーバオブジェクトのほかに引数も指定する必要があるため、このケースでは使用されない。