月: 2018年11月

  • Debian 9 Stretch で Docker のインストール

    Debian 9 Stretch で Docker のインストール

    Debian 9 Stretch に Docker をインストールして動作確認を行うまで。

    $ sudo apt update
    $ sudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common
    $ curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -

    /etc/apt/sources.list に次の一行を追加する。

    deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable

    もう一度 apt update して更新を行ったあとにインストールを行います。

    $ sudo apt update
    $ sudo apt install docker-ce

    動作確認に hello-world の image を run してみます。

    $ sudo docker run hello-world
    Unable to find image 'hello-world:latest' locally
    latest: Pulling from library/hello-world
    d1725b59e92d: Pull complete
    Digest: sha256:0add3ace90ecb4adbf7777e9aacf18357296e799f81cabc9fde470971e499788
    Status: Downloaded newer image for hello-world:latest
    Hello from Docker!
    This message shows that your installation appears to be working correctly.
    To generate this message, Docker took the following steps:
    The Docker client contacted the Docker daemon.
    The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
    The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
    The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.
    To try something more ambitious, you can run an Ubuntu container with:
    $ docker run -it ubuntu bash
    Share images, automate workflows, and more with a free Docker ID:
    https://hub.docker.com/
    For more examples and ideas, visit:
    https://docs.docker.com/get-started/

    ローカルのイメージを表示

    $ sudo docker images
    REPOSITORY TAG IMAGE ID CREATED SIZE
    hello-world latest 4ab4c602aa5e 2 months ago 1.84kB

    動作中のコンテナを表示

    $ sudo docker ps
    CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

    全てのコンテナを表示

    $ sudo docker ps -a
    CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
    4d4807dccc3c hello-world "/hello" 8 minutes ago Exited (0) 8 minutes ago friendly_chandrasekhar

    コンテナの削除

    CONTAINER ID を指定します。

    $ sudo docker rm 4d4807dccc3c
    4d4807dccc3c

    ローカルのイメージを削除

    イメージの名前か IMAGE ID を指定します。

    $ sudo docker rmi hello-world
    $ sudo docker rmi 4ab4c602aa5e

    参考

  • cannot update mailbox /home/user/Maildir for user user. cannot open file: Is a directory

    Postfix でメールサーバを建てましたが、メールの受信をテストをするために Gmail から自ドメイン宛にメールを送ってみたところ、次のようなエラーメールが返ってきました。

    調べたら原因はすぐに判明して、Postfix の設定の問題でした。Maildir 形式なのに mbox 形式として扱おうとしていたのが問題でした。

    /etc/postfix/main.cf

    ...
    home_mailbox = Maildir/
    ...

    home_mailbox = Maildir とするとmbox として、Maildir/ とすると Maildir と扱われるようです。ファイルかディレクトリかで判別しているということですね。

    参考

  • Debian 9 Stretch で locale の変更

    Debian 9 Stretch で locale の変更

    $ localectl
    System Locale: n/a
    VC Keymap: n/a
    X11 Layout: us
    X11 Model: pc105
    $ localectl list-locales
    C.UTF-8
    en_US.utf8
    $ sudo localectl set-locale LANG=en_US.UTF-8
    $ localectl
    System Locale: LANG=en_US.UTF-8
    VC Keymap: n/a
    X11 Layout: us
    X11 Model: pc105
  • Python WSGI で毎回 encode(‘utf-8’) するのが面倒

    Python WSGI で毎回 encode(‘utf-8’) するのが面倒

    Python の WSGI プログラムで出力をするたび

    yield '<h1>テスト</h1>'.encode('utf-8')
    yield '<p>あいうえお</p>'.encode('utf-8')

    と encode(‘utf-8’) を付けていましたが、どう考えてもこれは手間だし無駄な気がします。Flask を使えばいいと言われたらお終いなので、何か手はないかと考えてみました。

    Python にはデコレータ(decorator) という仕組があって、任意の関数の前後に好きな処理を追加することができます。関数も第一級オブジェクトである Python の特性をうまく利用した素敵機能ですね。

    yield を扱った decorator の例があまり見つからなくてやや苦戦しましたが、次のような decorator で実際に動作することを確認しました。

    def encode_utf8(func):
    	def wrapper(*args, **kwargs):
    		for r in func(*args, **kwargs):
    			yield r.encode('utf-8')
    	return wrapper

    前回の WSGI でフォームを扱うプログラムに適用させてみたのが次のコードです。

    # coding: utf-8
    
    from urllib.parse import parse_qsl
    
    def encode_utf8(func):
    	def wrapper(*args, **kwargs):
    		for r in func(*args, **kwargs):
    			yield r.encode('utf-8')
    	return wrapper
    
    @encode_utf8
    def application(env, res):
    	res('200 OK', [('Content-type', 'text/html; charset=utf-8')])
    
    	# Form
    	yield '<form method="get" accept-charset="UTF-8"><p><input type="text" name="get" value=""><input type="submit" value="GET"></form>'
    	yield '<form method="post" accept-charset="UTF-8"><p><input type="text" name="post" value=""><input type="submit" value="POST"></form>'
    
    	# Request method
    	method = env.get('REQUEST_METHOD')
    	yield '<p>Method: {}</p>'.format(method)
    
    	# Get query string
    	if method == 'POST':
    		wsgi_input = env['wsgi.input']
    		query = wsgi_input.read(int(env.get('CONTENT_LENGTH', 0))).decode('utf-8')
    	else:
    		query = env.get('QUERY_STRING', '')
    	yield '<p>Query: {}</p>'.format(query)
    
    	# Output form fields
    	form = parse_qsl(query)
    	for k, v in form:
    		yield '<p>{} = {}</p>'.format(k, v)

    行数は変らないのでぱっと見の変化少ないですが、書き忘れて Internal Server Error をもらう率は少し減りますね。

  • Python WSGI でフォームのデータを取得

    Python WSGI でフォームのデータを取得

    TL; DR

    POST

    query = env['wsgi.input'].read(int(env.get('CONTENT_LENGTH', 0))).decode('utf-8')

    GET

    query = env.get('QUERY_STRING', '')

    parse and split

    # dictionary
    form = urllib.parse.parse_qs(query)
    # list
    form = urllib.parse.parse_qsl(query)
    (さらに…)
  • プラグインに頼らない WordPress の移転

    プラグインに頼らない WordPress の移転

    経緯

    性能や管理の手間を考えてサーバの移転を行いました。その際に見事にハマった WordPress の移転について書きます。

    元々使っていたのは自宅サーバ上の WordPress 4.1 でした。バージョンが古いのは、楽にできるかと apt でインストールしてしまったこと、設置時にパーミッションや実行ユーザをぐちゃぐちゃにしていまい、更新できない状態であったことが理由です。

    セキュリティ上よろしくないのはもちろんのこと、そもそもプラグインやテーマを追加するのにサーバへファイルを直接転送しなければならないという、大変手間なことこの上ない状態になっていました。

    Vultr が日本で $3.5/月のインスタンスを用意してくれていたので、これを機に自宅サーバを廃止して VPS に全面移行することに決めました。

    自宅サーバでは自前のプログラム他、VPN や WordPress など様々なものを動かしていましたが、ハードの更新を何度か経験していたために自前のプログラムは特に問題にならないと思われ、VPN は不要になり、WordPress さえうまく移植できれば、ことはすぐに済むはずでした。

    WordPress にはインポートとエクスポート機能が標準で備わっているため、前回はこの機能を使ってすんなりと移植できたのをおぼえています。今回も、と思いきや、記事の文字部分はインポートされますが、何度やっても画像等のメディアファイルの取得に失敗してしまいます。

    次に WordPress では有名らしいプラグイン All-in-One WP Migration を使ってみましたが、約 700 MB 近い巨大なファイルを作り上げた上に記事の日本語が全て ? に化けてしまいました。

    その後も wp-content/uploads 内のファイルを自動登録してくれるプラグイン等、様々なものを試してみましたが、どれもどこかで不整合が起きて元通りには戻りません。そもそも標準のインポートがなぜだめなのか、原因は調べてもわからずじまいでした。

    そこで原点に戻って MySQL の DB を dump し、手作業で移植することにしたところ、ようやっとうまくいきました。その手順を以下に記します。

    環境

    移転元

    • MySQL 5.5.60
    • PHP 5.6.37
    • WordPress 4.1

    移転先

    • MariaDB 10.1.26
    • PHP 7.0.30
    • WordPress 4.9.8

    移転元での作業

    wp-content/uploads を固める

    $ tar cvzf /home/user/uploads.tar.gz /var/lib/wordpress/wp-content/uploads

    wp-content の場所は自分で探す必要があります。今回はなぜか Alias で通常とは違うところにあって、見つけるのに苦労しました。わからなかったら URL と Apache の conf をがんばって追って探すか、find 等で見つけましょう。

    $ find / -name wp-content -type d

    作成したアーカイブは scp 等で新しいサーバに転送しておきます。

    データベースの dump を作成する

    $ mysqldump --default-character-set=binary wordpress > wp.sql 

    WordPress のテーブルが独立したデータベース wordpress にある場合は上記で全てのテーブルが dump されます。別のテーブルが混ざっている場合は WordPress のテーブルだけを指定して dump してください。ユーザとパスワードは -u USERNAME -pPASSWORD を加えてください。

    作成した dump も新しいサーバに転送しておきます。

    “–default-character-set=binary” なしで dump すると、UTF-8 でも Shift-JIS でも EUC-JP でも日本語が読めないファイルが出来上がってしまいました。必ず指定しましょう。

    移転先での作業

    一度 WordPress 設置してインストールまで済ませ、WordPress が正常に動作することを確認しましょう。移転作業とインストールを同時に進めると、何かあったときに原因の切り分けが難しくなります。WordPress のバージョンは揃えた方が無難だと思いますが、今回は最新版の 4.9.8 を使ってそのままいけました。

    インストール時にデータベースやテーブルの接頭辞を指定しますが、必ず移転元と同じにしておきましょう。変更するなら WordPress の設定も変更しなければなりません。

    インストールが完了したら一点だけ確認しておきます。ログインしてダッシュボードを開き、「設定」 – 「メディア」の「ファイルアップロード」 – 「アップロードしたファイルを年月ベースのフォルダに整理」のチェックを移転元と同じ状態にしておきましょう。

    プラグインやテーマも揃えておく、あるいはコピーしてしまう方が安全だと思いますが、テーマに関してはサイトが表示されないだけで wp-login.php を開けばダッシュボードは開けるはずなので、今回は無視して進めました。

    dump から MariaDB へ

    WordPress のインストールまで済ませておくとテーブルのデータが競合してしまうので、全てのテーブルを空にしておきます。次の内容を clear_tables.sql などに保存しておきます。

    delete from wp_commentmeta;
    delete from wp_comments;
    delete from wp_links;
    delete from wp_options;
    delete from wp_postmeta;
    delete from wp_posts;
    delete from wp_term_relationships;
    delete from wp_term_taxonomy;
    delete from wp_terms;
    delete from wp_usermeta;
    delete from wp_users;
    $ mysql wordpress < clear_tables.sql
    $ mysql wordpress < wp.sql

    wp.sql は移転元で dump したデータベースの中身です。もし移転元と移転先のドメインが異なるなら、予めこのファイルをテキストエディタ等で開いてドメインを全て置換えしておきましょう。

    uploads の展開

    $ tar xvzf uploads.tar.gz
    $ cp -r uploads/* wp-content/uploads

    展開したファイルはパーミッションを適切に変更しておいてください。

    仕上げ

    uploads の移植とデータベースの復元が完了したら WordPress を開いてみましょう。データベース更新の案内が表示されるはずなので、あとは手順通り進めて完了です。お疲れさまでした。

    参考

  • Debian 9 Stretch で連続接続を Fail2ban ではじく

    Debian 9 Stretch で連続接続を Fail2ban ではじく

    Fail2ban は、ログを参照して認証エラーを繰返しているホストを ban するための Python 製のツールです。iptables で接続制御をするのは ufw と同じですが、Fail2ban はより柔軟にログを分析して接続を制御します。

    インストール

    $ sudo apt install fail2ban
    $ sudo fail2ban-client status
    Status
    |- Number of jail: 1
    `- Jail list: sshd

    設定

    /etc/fail2ban/jail.conf の中には次のような記述があります。

    # YOU SHOULD NOT MODIFY THIS FILE.
    #
    # It will probably be overwritten or improved in a distribution update.
    #
    # Provide customizations in a jail.local file or a jail.d/customisation.local.
    # For example to change the default bantime for all jails and to enable the
    # ssh-iptables jail the following (uncommented) would appear in the .local file.
    # See man 5 jail.conf for details.

    jail.cof は更新時に上書きされる可能性があるので、jail.local 等の別名ファイルを作成して差分を記述するようにしましょう、ということです。

    /etc/fail2ban/jail.conf 次のように書きます。ufw と組合せているので、ufw の limit を考慮しないとずっと ban されないことになります。

    [DEFAULT]
    bantime = 1800
    findtime = 600
    maxretry = 5
    
    [postfix-sasl]
    enabled = true
    
    [dovecot]
    enabled = true

    設定を再読込したら完了です。

    $ sudo systemctl reload fail2ban
    $ sudo fail2ban-client status
    Status
    |- Number of jail: 3
    `- Jail list: dovecot, postfix-sasl, sshd
    $ sudo fail2ban-client status sshd
    Status for the jail: sshd
    |- Filter
    | |- Currently failed: 1
    | |- Total failed: 7
    | `- File list: /var/log/auth.log
    `- Actions
     |- Currently banned: 0
     |- Total banned: 0
     `- Banned IP list:

    自分でフィルタやアクションを定義することもできますが、とりあえずはこれで様子を見てその都度変更していこうと思います。

    bantime

    接続を拒否する時間を秒で指定します。

    # "bantime" is the number of seconds that a host is banned.
    bantime = 600

    findtime と maxtretry

    findtime 秒間の間に maxretry 回の認証失敗があったときに拒否対象となります。標準の findtime = 600, maxretry = 5 であれば 600秒間に 5 回の試行で拒否対象となります。

    # A host is banned if it has generated "maxretry" during the last "findtime"
    # seconds.
    findtime = 600
    
    # "maxretry" is the number of failures before a host get banned.
    maxretry = 5

    参考