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

オープンソースこねこね

Webプログラミングなどについてあれこれ。

systemd-nspawnで/sys/fs/selinuxがマウントできないので警告される件は無視することにした

ただのメモです。

CentOS7でsystemd-nspawnの動作を検証してるなか、SELinuxを無効にした状態でコンテナを起動させると/sys/fs/selinuxがマウントできないと警告がでてきました。

# systemd-nspawn -D /var/tmp/mycontainer 
Spawning container mycontainer on /var/tmp/mycontainer.
Press ^] three times within 1s to kill container.
Failed to create directory /var/tmp/mycontainer//sys/fs/selinux: No such file or directory
Failed to create directory /var/tmp/mycontainer//sys/fs/selinux: No such file or directory
-bash-4.2# 

/sys/fs/selinuxSELinuxを無効にすると存在しないディレクトリ。調べたらsystemd-nspawnはHAVE_SELINUXマクロでこのディレクトリのマウントを制御しています

systemd/nspawn.c at v219 · systemd/systemd · GitHub

そしてログを出力しているところはココ

systemd/nspawn.c at v219 · systemd/systemd · GitHub

ソースを見る限りその後の処理は続行しているし、問題なさそうかな〜と考えていたら、

systemd-nspawn: dont try to mount non existing selinux directories by tblume · Pull Request #4569 · systemd/systemd · GitHub

で、同様の問題の当たっていたヒトがこの警告を消すためにログレベルをLOG_DEBUGからLOG_WARNINGにさげるPRを送っていてmasterにマージされていました。なので、今後CentOS(systemd)のバージョンが上がれば、この警告は消えるので、現状無視することにした、というお話でした。

CentOS7でサーバを構築する際におこなっている基本設定

CentOS

以前書いた

CentOS6でサーバを構築する際におこなっている基本設定 - オープンソースこねこね

のCentOS7バージョンです。調査中の内容もあるので、情報は随時更新する予定です。

※ここではサーバのロールにかかわらず行う基本的な作業をまとめています。通常この環境の上にWebサーバ(httpd, nginxなど)やDB(MySQLなど)をインストールすることになります。

※本文中のコマンドは基本的にrootユーザで行っているものとします。一般ユーザを使い必要に応じて都度root権限を使うようにしている方はsudoをつけるなど、コマンドを読み替えてください。

デフォルトターゲットの確認

systemctl get-defaultで取得。CentoOS6までのデフォルトランレベルに相当します。

# systemctl get-default
multi-user.target

multi-user.targetになっていることを確認します。

サービスの起動設定の確認

サービスの管理はsystemdによる管理に刷新されています。

systemctl list-unit-filesでサービスの一覧と状態を取得します。

# systemctl list-unit-files --type service --no-pager
UNIT FILE                                   STATE   
arp-ethers.service                          disabled
auditd.service                              enabled
auth-rpcgss-module.service                  static  
autovt@.service                             disabled
blk-availability.service                    disabled
brandbot.service                            static  
chrony-dnssrv@.service                      static  

STATEの意味は以下の通り。

  • enable: 自動起動有効
  • disabled: 自動起動無効
  • static: 他のユニットに依存。有効/無効が設定できないもの。

不要と思われるサービスはサーバリソースの無駄使いになるので、以下のコマンドで停止設定にしておきます。

# systemctl disable nginx.service
# systemctl stop nginx.service

OSの起動時に実行させたいサービスは

# systemctl enable nginx.service
# systemctl start nginx.service

としておきます。

ディクスサイズの確認

# df -h

サーバスペックにあったディクスサイズになっていることや、パーティションを確認します。

ロケールの設定

現在のロケールの設定を確認します。

# localectl status

利用可能なロケールlocalectl list-localesを調べ、

# localectl list-locales --no-pager

localectl set-localeで変更。

# localectl set-locale LANG=ja_JP.utf8

ロケールを日本語にしたとき、manページを日本語にするために

# yum install man-pages-ja

をインストールします。

タイムゾーンとNTPの設定

現在のタイムゾーンを確認します

# timedatectl status

利用可能なタイムゾーンtimedatectl list-timezonesで調べます

# timedatectl list-timezones --no-pager

timedatectl set-timezoneで変更。

# timedatectl set-timezone Asia/Tokyo

RTC in local TZ: yesの設定になっていると警告がでるので以下を設定する

# timedatectl set-local-rtc 0

CentOS7では時刻同期サービスが2つあって従来のntpdと新しいchronydがある。最小構成でOSインストールした場合は chronydだけがインストールされている状態になっているはず。それぞれ設定ファイルは

/etc/chrony.confまたは/etc/ntp.confです。設定ファイルのフォーマットはほとんど同じなので

server 0.centos.pool.ntp.org iburst

のような、時刻問い合わせ先サーバの設定を確認します。

# systemctl start ntpd.service
or
# systemctl start chronyd.service

でサービスを起動します。

最後にtimedatectlでntpによる時刻合わせを有効化します。

# timedatectl set-ntp yes

ネットワーク関連の設定

CentOS7ではネットワーク周りの管理がNetworkManagerを使った方式に変更されています。 NetworkManagerのコマンドラインインターフェースであるnmcliを使って設定するのが標準の方法ですが、 ひとまず従来CentOS6の方法でできることは、それを踏襲してます(非推奨の方法だと思うので、のちのち更新予定です)。

まず、従来のifconfigなどがデフォルトでは提供されていないのでnet-toolsパッケージをインストールします。

# yum install net-tools

ホスト名

従来/etc/sysconfig/networkに記載していたホスト名はCentOS7では/etc/hostnameに記述します。

ネットワーク・インターフェースの確認

CentOS6のときと同様に/etc/sysconfig/network-scripts配下のifcfg-xxxを確認します。 設定を変更したら、

systemctl restart network

で反映させます。

IPアドレスの確認

# ifconfig

IPアドレスの設定が、設定通りか確認します。

ネットワークの疎通確認

# ping yahoo.co.jp

pingが外に向けて通るかを確認します。0% packet lossとなればOK。

# traceroute yahoo.co.jp

外につなぎに行く際にどのようなIPを経由するかを確認。ネットワーク管理者ではないので、ざっと結果を眺めるだけです。

yumリポジトリ

CentOS6のときと同様。epelとremiは(個人的に)必須なのでインストールします。epelは直接yumからインストールできます。

# yum install epel-release
# yum install http://rpms.famillecollet.com/enterprise/remi-release-7.rpm

その他、設定ファイルなどはCentOS6のときと同様。

bash-completion

CentOS6のときと同様。素のbashのままだとzshになれた補完脳にはツライので、bash-completionを入れます。

# yum install bash-completion

インストールするだけで、ある程度補完が効くようになります。カスタマイズは特にしていません。

ファイアウォール (firewalld)

ファイアウォールiptablesからfirewalldに変わりました。 firewalldはiptablesをバックエンドにしたファイアウォール管理のフロントエンド的なもので、実は内部的にはiptablesが依然使われています。とはいえ、設定に関してはやることがガラっと変わっています。

設定のための覚書

firewalldにはゾーンという概念があり、サービスやポートへの接続許可、拒否などの設定をまとめた単位。ゾーンはデフォルトで

  • block
  • dmz
  • drop
  • external
  • home
  • internal
  • public
  • trusted
  • work

があります。ファイアウォールはこのゾーンをNICに割り当てることで、有効化され、機能します。 デフォルトではpublicゾーンが、有効化、割り当てされています。

# firewall-cmd --list-all

で確認できます。ゾーンについては

# firewall-cmd --list-all-zone

で一覧がでます。

デフォルトで有効になっているpublicゾーンは、明示的に許可した通信しか受け入れないようになっているので、基本的にこのゾーンに接続許可するポートやサービスの設定をしておけば、普通のファイアウォールの用途としては要件を満たせそうです。設定はfirewall-cmdを実行することで行います。

設定

いくつか典型的な設定パターンをメモしておきます。

特定の接続先(この例では192.168.0.1/24)からのみサービスへの接続(この例ではSSH)を許可する場合:

# firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.0.1/24" service name="ssh" accept' --permanent

サービスの定義は/usr/lib/firewalld/services/配下にあるxmlファイルを読む。ポート番号などがここに定義されています。

特定の接続先(この例では192.168.0.1/24)からのみポート8000の接続を許可する場合:

# firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.0.1/24" port port="8000" protocol="tcp" accept' --permanent

ポート80の接続を許可する場合:

# firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" port port="80" protocol="tcp" accept' --permanent

ちなみに上記のポートを単純に開放するだけなら

# firewall-cmd --zone=public --add-port=80/tcp --permanent

でも良さそう。個人的に設定の書き方を統一しておきたいので基本的に--add-rich-ruleを使っています。

設定を変更したら

# firewall-cmd --reload

で反映させます。その後確認のために

# firewall-cmd --list-all

でゾーンの設定を表示し、内容をチェックします。

sshd

CentOS6のときと同様。ポート番号を変えて、鍵認証オンリーにします。以下、編集および確認箇所のみを記載します。

/etc/ssh/sshd_config

# 22以外に変えます
Port 2222
# rootログインは禁止
PermitRootLogin no
# パスワード認証は禁止
PasswordAuthentication no

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

# systemctl restart sshd

ログの管理

syslogに代わる新しいログデーモンとしてjournaldが提供されています。systemdのサービスとして起動されたプログラムの標準出力、標準エラー出力は自動的にjournaldへと送られてログ管理の対象になります。ログはバイナリデータで保存され、専用のjournalctlコマンドを使って検索、表示させるという操作になります。

ログの保存場所は/var/run/log/journal配下。デフォルトで容量がファイルシステムの10%以上になると、古いものが削除されるようです。

WIP...設定内容を調査中...

logrotate

ログのローテート設定です。journaldにではなく、サービスが直接ファイルになどに出力したログファイルをローテートします。

CentOS6のときと同様です。ログが無尽蔵に大きくなってディクスサイズを圧迫しないように設定、確認します。 初期設定時の他にhttpdなどのデーモンやログを出力するアプリケーションを追加した時にも目を通します。

/etc/logrotate.d/以下にログの対象ごとに設定ファイルがあるのでそれを確認します。例えばnginxは以下のようになっていたりします。

/etc/logrotate.d/nginx

/var/log/nginx/*log {
    create 0644 nginx nginx
    daily
    rotate 10
    missingok
    notifempty
    compress
    sharedscripts
    postrotate
        /bin/kill -USR1 `cat /run/nginx.pid 2>/dev/null` 2>/dev/null || true
    endscript
}

実際の設定はログのサイズや保存期間の要件などによって異なるので、それを考慮し設定します。

sysctl

CentOS6と同様、カーネルパラメータの設定です。CentOS6のときは/etc/sysctl.confを編集してましたが/etc/sysctl.d/配下にxxx..confファイルを配置しても設定できるので、そうしてます。(実はCentOS6のときからできたっぽいですが。参考:http://qiita.com/ngyuki/items/002a4a153ac6d8075bdb)

/etc/sysctl.d/override.conf

net.core.somaxconn = 1024
vm.overcommit_memory = 1

設定の内容はCentOS6でサーバを構築する際におこなっている基本設定 - オープンソースこねこねを参照。設定値の反映は以下のコマンドを実行します。

# sysctl --system

参考: http://tsunokawa.hatenablog.com/entry/2015/09/02/175619

以下のコマンドで確認。

# sysctl -a

TODO

net.nf_conntrack_max = 1000000
net.netfilter.nf_conntrack_max = 1000000

が設定できなくなっている? 要調査...

Pythonの開発環境

MacPythonの開発環境を構築したかったので少しばかり調査、試行錯誤しました。調査メモと現時点で落とし所になった構成のまとめ。

調査

ダンpython開発環境は

  • python: ランタイム本体
  • pip: パッケージの管理コマンド(rubyのgem的な役割)
  • virtualenv: パッケージをディレクトリ単位で管理する仮想環境(rubyのbundler的な役割)
  • pyenv: 複数バージョンのpythonを管理(rbenvとかphpenvとかと同様)
  • pyenv-virtualenv: pyenvでvirtualenvの仮想環境を作成、管理するプラグイン

などが使われる模様。情報ソースは以下のサイトなどなど。

以下のオペレーションを実際にやってみました。

  • pyenvを使ってpythonをインストール
  • virtualenvで仮想環境を構築
  • pyenv-virtualenvで仮想環境を構築

所感。

  • 複数バージョンのpythonは今のところ私には必要ない。pyenvはなくていい。
  • 現状、やりたいことは、開発するときにプロジェクトディレクトリ配下にリソースをまとめたい。グローバルを汚染したくない、ということ。
  • virtualenvディレクトリに入ってからsource bin/activateしないと有効にならない。また出るときはdeactivateが必要。地味にめんどい。
  • pyenv-virtualenvでpyenv管理の仮想環境を作ってpyenv localでローカルの設定をしておけば、ディレクトリ移動で仮想環境が有効になるが、仮想環境自体が~/.pyenv配下に保存されて、インストールパッケージがプロジェクトのディレクトリ配下に配置されない。

いまひとつ不満が解消されないまま、さらに調べていたらいつも使っているdirenvが組み込みの関数でpythonのvirtualenvを構築、ロードに対応していることを発見。

https://github.com/direnv/direnv/blob/c2cfad5ee664fed68224c44fd0549e73615f6b79/stdlib.sh#L352

試してみたらいい感じだったので、現状これとhomebrewでインストールできるpythonを使う構成に落ち着きました。以下、その環境の詳細となります。

環境構築

最終的な構成は以下になります。

  • python (version2.x)
  • pip
  • virtualenv
  • direnv

インストール手順。まずはpythonをインストール。Mac標準のよりバージョンが新しくpipもバンドルされてるのでhomebrewで提供されているものを使います。direnvもhomebrewでインストールする。

$ brew install python
$ brew install direnv

virtualenvはpipでシステムにインストール。

$ pip install virtualenv

システムにインストールするものは以上です。

開発環境の使い方

プロジェクトディレクトリの作成

何か開発したいときはプロジェクトディレクトリを作成して、

$ mkdir python_project
$ cd python_project

layout_pythonを記述した.envrcファイルを用意する。

$ echo "layout_python" > .envrc && direnv allow

すると

direnv: loading .envrc                                                                                                            
Running virtualenv with interpreter /usr/local/bin/python
New python executable in /path/to/python_project/.direnv/python-2.7.12/bin/python2.7
Also creating executable in /path/to/python_project/.direnv/python-2.7.12/bin/python
Installing setuptools, pip, wheel...done.
direnv: export +VIRTUAL_ENV ~PATH

となってpython_project/.direnvにvirtualenvの仮想環境が構築された上に、VIRTUAL_ENV環境変数が設定され仮想環境が有効になります。 (source bin/activateしたのと同様の状態になる)

パッケージのインストール

試しにpipでこの仮想環境にインストール済みのパッケージを確認してみます。

$ pip list
pip (8.1.2)
setuptools (27.2.0)
wheel (0.30.0a0)

そこに新しくパッケージをインストールします。

$ pip install supervisor

もう一度pip listすると

$ pip list
meld3 (1.0.2)
pip (8.1.2)
setuptools (27.2.0)
supervisor (3.3.1)
wheel (0.30.0a0)

supervisorがインストールできていることが確認できます。そしてこのプロジェクトディレクトリから出て、グローバルでのパッケージを確認すると

$ cd ..
$ pip list
mercurial (3.9.1)
pip (8.1.2)
setuptools (23.1.0)
vboxapi (1.0)
virtualenv (15.0.3)
wheel (0.29.0)

プロジェクトでインストールしたものが入っていないことが確認にできます。再びプロジェクトディレクトリに入ると、仮想環境が有効になり、インストール済みパッケージももとに戻ります。

パッケージの書き出しと復元

pip freezeでプロジェクトにインストールしたパッケージを書き出しておき、

$ pip freeze > requirements.txt

.direnvを削除して、仮想環境を消去し、一度プロジェクトディレクトリを出ます。この状態でもう一度プロジェクトディレクトリに入ると

$ rm -rf .direnv 
$ cd ..
$ cd python_project
direnv: loading .envrc                                                                                                            
Running virtualenv with interpreter /usr/local/bin/python
New python executable in /path/to/python_project/.direnv/python-2.7.12/bin/python2.7
Also creating executable in /path/to/python_project/.direnv/python-2.7.12/bin/python
Installing setuptools, pip, wheel...done.
direnv: export +VIRTUAL_ENV ~PATH

仮想環境が再作成されます。最後にpip install -rで書き出したパッケージ情報から、パッケージを再インストールすれば、環境が復元できました。

$ pip install -r requirements.txt

おわりに

pythonに複数バージョンが必要になったらこの構成にpyenvを足すだけで良さそう。

direnv便利。

CentOSにLet's EncryptのSSL証明書を導入する

やってみたので、手順をまとめました。実施したのはCentOS6+Apache2.2という、やや古い環境ですがCentOS7とかでも基本的には同じはずです。

Let's Encryptの概要

Let's Encryptは無料のSSL証明書を発行する認証局。 特徴は料金無料であるのと、専用のクライアントコマンドを使って証明書の発行を行う点。またワイルドカードの証明書は発行できない。有効期限が3ヶ月で、更新もコマンドによって自動化するのが前提となっています。

証明書を発行する際の要件として、当然ドメイン所有者の認証(本当にそのドメインの所有者か確認を行うこと)が必要になります。通常の認証局はメールなどを使ってこれを行いますが、Let's EncryptはACMEプロトコルという通信プロトコルと専用のクライアントコマンドを使います。ACMEプロトコルについて以下の記事が参考になりました。

Let's Encrypt を支える ACME プロトコル - Block Rockin’ Codes

ざっくり理解したところでは、ACMEプロトコルの内容は難しいものではなく、HTTPをベースに使った証明書の自動発行手順といった感じのものです。

さて、実際にLet's Encryptの証明書を導入するには一般に以下の作業が必要です。

  • クライアントコマンドのインストール
  • HTTPサーバの用意
  • クライアントコマンドを実行して証明書を取得
  • 取得した証明書使うためのHTTPサーバの設定
  • 自動更新のための設定

以下、順に記述します。

手順

クライアントコマンドのインストール

ACMEプロトコルでLet's Encryptのサーバと通信して証明書発行を行うCLIツールです。ここでは公式が提供しているcertbot-autoというツールを使います。

https://certbot.eff.org/

シェルスクリプトPythonで構成されています。このツールの操作自体、基本的にroot特権が必要になるので、以下のインストール作業のコマンドも全てrootユーザで行いました。

# yum install -y epel-release
# curl https://dl.eff.org/certbot-auto -o /usr/bin/certbot-auto
# chmod a+x /usr/bin/certbot-auto
# certbot-auto --os-packages-only --non-interactive

最後のcertbot-auto --os-packages-only --non-interactiveで依存パッケージがインストールされます(pythonのランタイムとか)。

HTTPサーバの用意

証明書発行時にACMEプロトコルドメインの認証処理(acme challengeというらしい)において、証明書を発行するドメインに対してHTTPでリクエストを投げてきます。 よってHTTPサーバが稼働している必要があります。ここではapache使うことにします。

# yum install -y httpd

インストールしたらドメイン名でアクセスできるように設定しておきます。

クライアントコマンドを実行して証明書を取得

certbot-auto certonlyで証明書を取得します。証明書発行に必要な秘密鍵などもこのコマンドを叩くだけでまとめて生成されます。

# certbot-auto certonly --non-interactive --agree-tos --webroot -w /var/www/html -d example.com --email kohki.makimoto@gmail.com

以下のオプションは環境に合わせて読み替えてください。

  • -d: ドメイン名を指定します。

  • --email: 更新期限が近づいたときにメール通知されるメアドを指定します。

  • -w: HTTPサーバのドキュメントルートを指定します。

-wオプションについて補足します。前述したようにACMEプロトコルドメインの認証処理でHTTPサーバにアクセスしてくるのですが、その際/.well-known/acme-challenge/というパスに対してアクセスしてきます。クライアントコマンドはこのパスにLet's Encrypt が指定したテキストを配置する必要があるので、そこにアクセスできるようドキュメントルートを指定するわけです。

その他オプションについての詳細はcertbot-auto --help allで確認してください。

うまくいくと

/etc/letsencrypt/live/{ドメイン名}

というパスに証明書や秘密鍵が生成されます。

取得した証明書使うためのHTTPサーバの設定

最近はSNIを使ってバーチャルホストごとにSSL設定ができるので、SSL証明書の設定はバーチャルホストごとに設定しました。

<VirtualHost *:443>
    ServerName example.com
    # ...

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/example.com/cert.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
    SSLCertificateChainFile /etc/letsencrypt/live/example.com/chain.pem

</VirtualHost>

なお、サーバ全体のSSL設定は/etc/httpd/conf.d/ssl.confなどに以下のように記述しておきます。

LoadModule ssl_module modules/mod_ssl.so

AddType application/x-x509-ca-cert .crt
AddType application/x-pkcs7-crl    .crl

SSLPassPhraseDialog  builtin
SSLSessionCache         shmcb:/var/cache/mod_ssl/scache(512000)
SSLSessionCacheTimeout  300
SSLMutex default
SSLRandomSeed startup file:/dev/urandom  256
SSLRandomSeed connect builtin
SSLCryptoDevice builtin
SSLStrictSNIVHostCheck off

Listen 443

また、必要に応じてhttpにアクセスがあったらhttpsにリダイレクトさせる設定もいれます。

<VirtualHost *:80>
    ServerName example.com
    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R,L]
</VirtualHost>

自動更新のための設定

certbot-auto renewコマンドで証明書の更新ができます。Let's Encryptの証明書は3ヶ月で有効期限が切れるので、cronに以下のコマンドを仕込みます。この例は出力を捨ててますが、ちゃんと運用する場合はきちんとログに残しましょう。

50 3 * * * root certbot-auto renew --post-hook "/usr/sbin/apachectl graceful" > /dev/null 2>&1

証明書取得時のようにドキュメントルートの指定はしていないので、/etc/letsencrypt/配下の設定ファイルなどを見て更新しているっぽいです。

–-force-renewをつけて実行すると有効期限が1ヶ月未満にならなくても、強制的に有効期限を更新します。手動で更新検証するのに使いました。

以上で一通り、作業は完了です。

その他

Basic認証をかけたサイトにLet's Encryptを使う

Basic認証をかけているサイトだとACMEプロトコルドメイン認証で、HTTPサーバにアクセスできず、証明書取得ができません。 以下の設定を入れて/.well-known/acme-challengeに穴をあけておくとうまくいきます。

<Location />
    Satisfy Any
    AuthType Basic
    AuthName "My Private Stuff"
    AuthUserFile /var/www/example/.htpasswd
    Require valid-user

    SetEnvIf Request_URI "/.well-known/acme-challenge" acme-challenge
    Order Deny,Allow
    Deny from all
    Allow from env=acme-challenge
</Location>

サードパーティツール

クライアントツールはサードパーティーのGo実装のシングルバイナリもあるのですが、現状見送り。公式のほうがやはり安心感ある。

CentOS6のpythonのバージョンが古い

CentOS6はpythonのバージョンが古いのでcertbot-auto実行時に以下のような警告がでました。が、正常に証明書取得までできたのでまあいいかな、と。。。

/root/.local/share/letsencrypt/lib/python2.6/site-packages/cryptography/__init__.py:26: DeprecationWarning: Python 2.6 is no longer supported by the Python core team, please upgrade your Python. A future version of cryptography will drop support for Python 2.6
  DeprecationWarning

いろいろ参考にさせていただいたサイトなど

ありがとうございました。

Go+LuaでHTMLからPDFを生成するCLIツールを作った

github.com

以前、Goでプロビジョニングツールを作った でも書いたように、GoとGopherLuaCLIツールをいろいろ実装していて、今回もその成果物の一つです。

概要

Goで書かれたワンバイナリで動くPDF生成ツールです。現時点で動作環境は64bitのMacLinuxのみをサポートしてます(windowsはまだ)。全体的にコードの実装はcofuからコピペして雑に作ったので、使いかたもほぼ同じです。

html2pdfというコマンドをreleaseページからダウンロードしてPATHの通ったディレクトリに配置し、

local html2pdf = require "html2pdf"

local example = html2pdf.pdf "example.pdf"
example.options = {
    page_size = "A4",
}
example.pages = {
    input_content = "hello world!"
}

のようなLuaのコードをexample.luaに保存。html2pdf example.luaを実行する。

$ html2pdf example.lua
==> Starting html2pdf...
==> Loaded 1 pdf config.
==> Evaluating example.pdf
    output_file: example.pdf
==> Complete!

これでexample.pdfが出力されます。上記の例だとコンテンツを設定するinput_contentに単純な文字列(hello world!)を指定しているだけですが、実際にはHTMLを書いて使います。

使い方

URLを指定

input_contentの代わりにinputを使えばURLを指定できます。

local html2pdf = require "html2pdf"

local example = html2pdf.pdf "example.pdf"
example.pages = {
    input = "https://github.com/kohkimakimoto/html2pdf"
}

複数ページ

pagesには複数のページを設定できます。

example.pages = {
    { input = "https://github.com/kohkimakimoto/html2pdf" },
    { input = "https://github.com/kohkimakimoto/cof" },
}

表紙

coverで表紙を設定できます。ページの指定と同じくinput_contentinputでコンテンツを指定できます。

example.cover = {
    input_content = "<b>title</b>"
    -- or
    -- input = "https://..."
}

その他オプション

html2pdfは内部でwkhtmltopdfを使っています。というより、実装的にはwkhtmltopdfにLuaスクリプティング機能を載せたラッパーコマンドになっています。なのでwkhtmltopdfのオプションが使えます。optionsで以下のようにして指定します。

example.options = {
    page_size = "A4",
    margin_left = 5,
    margin_top = 5,
    margin_bottom = 5,
    margin_right = 5,
    orientation = "Landscape",
    -- etc...
}

詳しくはwkhtmltopdfのドキュメントを参考にしてください。wkhtmltopdf docs

変数

-var-var-fileオプションで実行時にコマンドラインから変数を渡せます。変数はJSONで以下のように指定します。

$ html2pdf example.lua -var='{"output_file": "foo.pdf"}'

スクリプト内ではvar変数として格納されています。

local html2pdf = require "html2pdf"

local example = html2pdf.pdf "example.pdf"
example.output_file = var.output_file
example.pages = {
    input = "https://github.com/kohkimakimoto/html2pdf"
}

Shebang

#!/usr/bin/env html2pdf

スクリプトファイルの先頭に記述して実行権限をつければ、直接実行してPDFを吐くスクリプトになります。

Markdown

MarkdownからHTMLを生成するLuaモジュールも組み込んだので、すこしだけLuaでロジックを書けばmarkdownからPDFドキュメントを作ることもできます。以下にサンプルを作ったので参考にしてください。

https://github.com/kohkimakimoto/html2pdf/tree/master/_example

表紙付きドキュメントを生成するサンプルには結果のPDFもアップロードしてあります。

https://github.com/kohkimakimoto/html2pdf/blob/master/_example/doc/doc.pdf

DSLな記法

設定はLuaDSLスタイルでも記述することもできます。次の2つの例は同じ設定です。もりもりコードを書くのでなければ、DSLのほうが見やすいかもしれません。

DSLLua:

pdf "hello.pdf" {
    pages = {
        input_content = "hello world!"
    },
}

PlainなLua

local html2pdf = require "html2pdf"

local hello = html2pdf.pdf "hello.pdf"
hello.pages = {
    input_content = "hello world!"
}

DSLで使っているpdf関数はhtml2pdf.pdfメソッドのエイリアスになっています。

実装の話

当初、PDF変換部分のwkhtmltopdfを調べていたら、これはCで書かれたプロダクトだったので、Goからcgoをつかって連携しようかと考えて調査したのですが、どうも共有ライブラリをロードするGoのプログラムはスタンドアロンバイナリにならないようだと判明。あとクロスコンパイル周りで罠があったりとか、Macではcgoをつかってスタティックなビルドができないとか。。。(結構いろいろ調べたのですが、もう詳細忘れた。。。とにかくできそうになかった。自分のスキルが足りない可能性ももちろんありますが)。cgoツライ。

html2pdfを使うマシンにwkhtmltopdfをインストールしておけばいいだけなのですが、これでは当然、スタンドアロンなシングルバイナリとはいえないわけです。これはイヤだなと。

で、wkhtmltopdfコマンド自体がスタンドアロンバイナリなので、もうこのバイナリ自体をまるごとgo-bindataでGoのコードに埋め込んでおき、実行時に/tmpディレクトリ以下にwkhtmltopdfのコマンドファイルを生成して、それをhtml2pdfから叩くという荒業を使うことにしました。これで見た目上はhtml2pdfのみで動いているように見えるし、別途wkhtmltopdfをインストールする必要もない。

自分は何に使っているのか

個人事業主やってるので、クライアントに提出する請求書をPDFで生成するのに使っています。

Mac OSX El Capitanにphpenvをいれる

phpenvをMacにいれました。PHPのビルド時にいろいろエラーが出てツライ目にあいました。いろいろやることあります。詳細は末尾の参考サイトを参照してください。ここにはやった結果だけ記述します。

依存物のインストール。

$ brew install re2c
$ brew install openssl
$ brew install bison
$ brew install libxml2

libzがエラーを出すので以下のコマンドを叩きます。

$ xcode-select --install

phpenv本体とphp-buildのインストール。phpenvは知名度の高いCHH/phpenvではなくてmadumlao/phpenvをいれます。こちらのほうがプロダクトとして高品質なので。

$ git clone https://github.com/madumlao/phpenv.git ~/.phpenv
$ git clone https://github.com/php-build/php-build.git ~/.phpenv/plugins/php-build

.zshrcにパスとか必要な環境変数の設定

export PATH="$HOME/.phpenv/bin:$PATH"
eval "$(phpenv init -)"

export PHP_BUILD_CONFIGURE_OPTS="--with-openssl=$(brew --prefix openssl) --with-libxml-dir=$(brew --prefix libxml2)"

シェルをリロードしてから、phpをインストールします。ひとまずphp.5.6のこの時点での最新版。

$ phpenv install 5.6.23 

インストールが成功したら標準で使うphpをそれに設定します。

$ phpenv global 5.6.23
5.6.23

バージョンを確認。

$ php -v
PHP 5.6.23 (cli) (built: Aug  4 2016 13:23:46) 
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies
    with Zend OPcache v7.0.6-dev, Copyright (c) 1999-2016, by Zend Technologies
    with Xdebug v2.3.3, Copyright (c) 2002-2015, by Derick Rethans

参考サイト

SSL証明書の発行手順

Webサーバとかに配置するSSL証明書。毎度手作業なので自分用にメモ。

いつも「ドメイン名.拡張子」というファイル名で秘密鍵CSR、証明書を作っています。

作業手順

パスフレーズなしの秘密鍵を作成。

$ openssl genrsa -out example.co.jp.key 2048

秘密鍵を指定してCSRの作成。適宜項目を埋める。

$ openssl req -new -key example.co.jp.key -out example.co.jp.csr
Country Name (2 letter code) [GB]:JP
State or Province Name (full name) [Berkshire]:Tokyo
Locality Name (eg, city) [Newbury]:shinjuku-ku
Organization Name (eg, company) [Internet Widgits Pty Ltd]:your company name
Organizational Unit Name (eg, section) []: your section
Common Name (eg, your name or your server's hostname) []:example.co.jp
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

CSRから自己署名で証明書を作成(本番の時は認証局CSRを提出)。

$ openssl req -in example.co.jp.csr -key example.co.jp.key -x509 -out example.co.jp.crt

メモ

  • 更新時、鍵に変更がなければCSRは使いまわせる。
  • そろそろLet's Encryptについても触っておきたい。。。