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
というツールを使います。
シェルスクリプトと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
いろいろ参考にさせていただいたサイトなど
- Let’s EncryptのSSL証明書で、安全なウェブサイトを公開 - さくらのナレッジ
- Let's Encrypt を支える ACME プロトコル - Block Rockin’ Codes
- Go言語でLet's EncryptのACMEを理解する | SOTA
- Support HTTP Basic Authentication with webroot · Issue #1744 · certbot/certbot · GitHub
ありがとうございました。
Go+LuaでHTMLからPDFを生成するCLIツールを作った
以前、Goでプロビジョニングツールを作った でも書いたように、GoとGopherLuaでCLIツールをいろいろ実装していて、今回もその成果物の一つです。
概要
Goで書かれたワンバイナリで動くPDF生成ツールです。現時点で動作環境は64bitのMacとLinuxのみをサポートしてます(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_content
かinput
でコンテンツを指定できます。
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な記法
設定はLuaのDSLスタイルでも記述することもできます。次の2つの例は同じ設定です。もりもりコードを書くのでなければ、DSLのほうが見やすいかもしれません。
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
$ 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についても触っておきたい。。。
CentOS用にSupervisor3.3.0のRPMパッケージを作成した
Pythonも同封した、他パッケージへの依存がないsupervisorのRPMパッケージをOmnibusで作成してみました。
背景
CentOSの各バージョン(5, 6, 7)でSupervisorを使いたい、のだが。。。
CentOSではepelのyumリポジトリからsupervisorのRPMパッケージが提供されているというので、調べてみたのですが、CentOS5と6だとかなりバージョンが古い。提供されているのはバージョン2.xで、今の最新は3.3.0。かといってpythonのpipを使って最新をシステムワイドのpythonパッケージとしてインストールするのはイヤだなあと。
システムへのソフトウェアインストールはyumやrpmなどのプラットフォーム標準のパッケージ管理システムをなるべく使うというポリシーです。特にデーモンなどは、実用するならinitスクリプトやlogrotateの設定も合わせてインストールしなければならないし。
またSupervisorを動かすのにpythonが必要なのですが、これのバージョンがCentOS5,6,7それぞれ違っていて、しかもCentOS5はpython2.4。古!
要するに古いCentOSの運用はつらい。
とにかく、CentOS5,6,7の環境で同じバージョン(最新のバージョン)のSupervisorを動かしたい。そこでOmnibusです。
Omnibus
chef社が提供している、依存物をまるごと同封したパッケージをクロスプラットフォームで作ってくれるツールです。
これを利用しているソフトウェアは、有名ドコロだとchefやtd-agentなど。これらのソフトウェアはRubyで書かれているのですが、OmnibusでRubyのランタイムを同封したパッケージを作って、ユーザに提供しています。このためユーザは別途Rubyをインストールする必要がなく、複数のパッケージによる管理の複雑さや依存性の問題を気にする必要がなくなります。余談ですが、これでパッケージングされたchef-soloが提供された時、劇的にインストールが簡単になって大変便利になったと記憶しています。
さて、Omnibusの利用方法は公式リポジトリのREADMEと、td-agentなどの利用例を参考にすればいいと思います。というか、私自身がそのようにして作業してました。個人的に特に参考にさせていただいたリポジトリは以下。
- https://github.com/treasure-data/omnibus-td-agent
- https://github.com/Netuitive/omnibus-netuitive-agent
Omnibusではソフトウェアコンポーネントという単位で、パッケージングするソフトウェアの情報を定義するのですが、幸いなことにGithubで検索すると、先人の方々がpythonのそれを作っていて、そのへんも参考にさせていただきました。また公式に主要なソフトウェアコンポーネントの設定が公開、共有されていて、これも簡単に利用できるようになっています。
とはいえ、CentOS5はOS自体がやや古いのでOmnibusに必要なパッケージが直接ダウンロードできなかったりして、いろいろワークアラウンドをいれたりしてます。詳細は最初に記載した私のリポジトリのソースを見ていただければと思います。
本来はRPMだけでなくdebパッケージなども作成できるのですが、今回は自分が使うRPMのみを作成するように設定しました。
細かい調整など
supervisordを起動するinitスクリプトは当初epelのsrc.rpmを解凍して、中に入っていたものを使用していたのですが、reload処理がstopしてstartするというrestartと同じものになっており、これはイケてないな、と。 調べていたらユーザによるinitスクリプト集がsupervisorのリポジトリにあったので、こちらを参考に修正したものを使いました。
GitHub - Supervisor/initscripts: User-contributed OS init scripts for Supervisor
積み残し
Omnibusで作成したパッケージはデフォルトで/opt/{PACKAGE_NAME}
のディレクトリ配下に全ての依存物もまとめてイントールされます。これはこれでいいのですが、initスクリプトなど、このディレクトリ以外の領域にファイルを配置する場合が悩ましく、現状postinst
というパッケージインストール後に実行されるスクリプトの設定を使ってcp
コマンドでコピーするという方式をとっています。
cp -f /opt/supervisor/etc/init.d/supervisord /etc/init.d/supervisord
他の参考にさせていただいたリポジトリも私が調べた限りでは、同様の手法で対応しているのですが、この方法だと少し問題があって、パッケージがインストールしたファイル一覧を見るコマンドrpm -ql
の結果にコピーしたファイルが表示されないのです。
$ rpm -ql supervisor /opt /opt/supervisor /opt/supervisor/LICENSE /opt/supervisor/LICENSES /opt/supervisor/LICENSES/bzip2-LICENSE /opt/supervisor/LICENSES/cacerts-README ... ... (/etc/init.d/supervisord は出てこない。。。)
カスタムのspecファイルを書いたり、いろいろ試行錯誤してみましたがうまくいかず、ベストな方法は今のところわかっていません。 これについては実用上、明らかな問題にはならないので、現状、課題として積んでおいています。
成果物
作成したRPMファイルはgithubのreleasesページにアップロードしました。
https://github.com/kohkimakimoto/omnibus-supervisor/releases
これで、インストールはyumコマンド一発でできるし、外部に依存関係をもたないので、必要なくなったら他への影響を考えずにサクッとアンインストールもできるようになりました。CentOSのバージョンも5~7まで対応。ただしちょっとファイルサイズは大きいです。
インストールして動作させてみましたが、ひとまず問題はなさそうです。
Goでプロビジョニングツールを作った
GitHub - kohkimakimoto/cofu: Minimum configuration management tool written in Go.github.com
CofuというサーバプロビジョニングツールをGoで実装しました。Itamaeを参考に作りました。実装言語の違い(ItamaeはRubyによる実装)はありますが、外部仕様、内部実装、共にかなり似せて作ってあるので、ItamaeまたはItamaeが参考にしているchefを使ったことがあると、理解は簡単かと思います。特徴をざっくり説明すると、
- ローカルでのプロビジョニングのみ対応。SSHなどでのリモートサーバのプロビジョニングはサポートしない
- Goなので実行ファイル一個で動く。導入が簡単
- レシピはLuaのDSLで記述する
- 今のところ動作プラットフォームはRedHat(CentOS)のみをサポート
あたりでしょうか。以下に軽く使い方を記載します。
インストール
ビルド済みバイナリがありますので
https://github.com/kohkimakimoto/cofu/releases
からファイルをダウンロードして解凍し、パスの通ったディレクトリに配置するだけでOKです。RPMパッケージも用意してありますが、これは/usr/bin/cofu
をインストールしているだけなので、利用はお好みでどうぞ。
使い方
レシピファイルをこんなふうに記述して、
-- recipe.lua software_package "httpd" { action = "install", } service "httpd" { action = {"enable", "start"}, }
cofu
コマンドの引数に指定して実行します
$ sudo cofu recipe.lua ==> Starting cofu... ==> Loaded 2 resources. ==> Evaluating software_package[httpd] software_package[httpd]: 'installed' will change from 'false' to 'true' ==> Evaluating service[httpd] service[httpd]: 'enabled' will change from 'false' to 'true' service[httpd]: 'running' will change from 'false' to 'true' ==> Complete!
レシピを指定して、実行する。これだけ。
リソース
Itamaeと同様にレシピファイルにはサーバの状態をリソースという単位で記述します。以下が実装済みのリソースです。だいたいItamaeで用意されているのと同じです。
- directory
- execute
- file
- group
- link
- lua_function
- remote_file
- service
- software_package
- template
- user
実体はLuaの関数なのですが、リソース記述のsyntaxは以下のようになります
resource_type "name" { attribute = "value", action = "type_of_action", }
Tips
では、もうちょっと実用的な使い方やサンプルなどを幾つか紹介
dry-run
-n
オプションをつければdry-runで実行します。実際には変更が行われないモードですね。変更内容の確認のために使います。
$ sudo cofu -n recipe.lua
executeリソースでコマンド実行
コマンドを実行します。not_if
とonly_if
は全てのリソースで使用できる共通のアトリビュートです。
execute "echo hello > /tmp/example" { not_if = "grep hello /tmp/example", -- このコマンドが失敗した時のみ実行される -- only_if = "grep -v hello /tmp/example", -- このコマンドが成功した時のみ実行される }
templateで設定ファイルを更新
template
リソースで設定ファイルを更新します。テンプレートはGoのtext/templateで記述します。
template "/etc/httpd/conf.d/foo.conf" { owner = "root", group = "root", mode = "0644", source = "foo.conf.tmpl", }
source
を指定しない場合、配置先のファイルパスからデフォルトでテンプレートファイルの位置を決定します。上記の例ですとレシピファイルからの相対パスで
templates/etc/httpd/conf.d/foo.conf
または
templates/etc/httpd/conf.d/foo.conf.tmpl
を使用します。
remote_fileでファイルを配置
テンプレート処理が必要なく、単純にファイルを配置したいだけのときはremote_file
リソースを使います。
remote_file "/path/to/hoge.zip" { owner = "root", group = "root", mode = "0644", source = "hoge.zip", }
この場合もsource
を指定しない場合はデフォルトのパスを使用します。上記の例ですと
files/path/to/hoge.zip
となります。
変数
chefやItamaeでいうところのノードアトリビュートにあたるものとして、実行時にコマンドラインから任意の変数を割り当てられます。-var
オプションをつかってJSONを直接指定します。
$ cofu recipe.lua -var='{"name": "kohkimakimoto"}'
これでテンプレートなどでは
{{var.name}}
で値を参照できます。レシピ内ではLuaのグローバル変数var
に割り当てられるので
var.name
で参照できます。
JSONを直接文字列からではなくファイルから読みたいときは-var-file
オプションを使用してください。
$ cofu recipe.lua -var-file=variables.json
パッケージのインストールとサービスの起動
software_package
でパッケージのインストール、アンインストール。service
でサービスの起動、停止、再起動ができます。software_package
はItamaeやchefでいうところのpackage
リソースと同等なのですが、Luaだとpackage
がビルトインのモジュールが利用している予約語なので、名前を変えています。
software_package "httpd" { action = "install", } service "httpd" { action = {"enable", "start"}, }
notifiesでリソース更新に合わせて別のリソースのアクションを実行する
設定ファイルが更新されたらサービスをリロードさせたりするのに使います。
template "/etc/httpd/conf/httpd.conf" { owner = "root", group = "root", mode = "0644", notifies = {"reload", "service[httpd]"}, } service "httpd" { action = "nothing", }
Itamaeだとこれの逆バージョンのsubscribes
(別のリソースの更新を検知してアクションを実行する)があるのですが、個人的にnotifies
しか使っていなかったのでCofuではnotifies
のみを実装しています。
別のレシピを読み込む
include_recipe
関数で別のレシピを読み込みます。
include_recipe "nginx.lua" include_recipe "php.lua"
ドキュメント
まだまだ欠けていますが、ここにあります。
https://github.com/kohkimakimoto/cofu/blob/master/docs/README.md
動作プラットフォームについて
前述のようにCentOSでしか動作確認、サポートしていません。
単純に私が他のOSは普段使っていないので今のところ対応しないというだけで、プラットフォームごとに処理を切り分ける部分はItamaeが利用しているSpecinfraを参考にして実装してあるので、マルチプラットフォーム対応ができる構造にはなっています。Specinfraを参考にプラットフォームによって異なる部分のコードを追加すれば、対応できるはずです。
ただテストを自動化してないので、本格的にマルチプラットフォーム対応させる際にはそのへんも考えていかなくてはいけないのかも。
開発の経緯と実際のユースケース
Itamaeに比べて機能は少ないし、SSHバックエンドをサポートしてないし、対応プラットフォームもCentOSのみというプロダクトなので、機能面でいうとこれは劣化版Itamaeという趣になっています。実際のところ私自身今はプロビジョニングツールにItamaeを使っていて、既存のItamaeのレシピをCofuで置き換えるつもりはなく、今後もItamaeを使い続ける予定であります。
実はこのCofuを作成した経緯は、フルスタックなプロビジョニングツールを作ろうと思っていたわけではなく、 そもそもの発端は「アプリケーションのデプロイ時に、サーバーのミドルウェアの設定ファイルを更新したい」という要件でした。
具体的なもののひとつがcronの設定ファイルで、これにアプリケーション固有のバッチ処理などが定期実行されるように記述されているわけです。アプリでバッチ処理を追加する場合、アプリケーションのリポジトリとサーバーコンフィグレーションを管理しているItamaeレシピのリポジトリの二つにコミットする必要がありました。
またアプリ固有の処理なので、プログラムロジック自体も特定の定期実行のスケジューリングを前提にした作りになっていることも多々あり、スケジューリングの設定と本質的に不可分で、この場合cronの設定自体アプリケーションのレイヤで管理すべきものです。それができないため、バッチプログラムのコメントに「日次バッチ。cronで実行される(/etc/cron.d/app参照)」とかメモを書いていたりしました。
こういう背景から、アプリケーション側に寄せるべき設定を管理するために、デプロイ時に特定の設定ファイルを書き換える仕組みがほしいと思っていて、最初はシェルスクリプトを書いていたのですが、いろいろ考えていくと冪等性が欲しかったり、設定ファイルが更新された場合にはコマンドを実行したりする必要が出てきたりして、Itamaeのようなプロビジョニングツールと機能要件が似てきたという次第です。
まあそれならデプロイ時にitamae
コマンドを叩けばいい、というのも当然あるのですが、設定ファイル一個をリポジトリルートにおいておいてデプロイ時にこれを実行すればOKというシンプルな仕組みが欲しかったので、結局最後まで作りました。以下がそのサンプルです
-- config_cron.lua template "/etc/cron.d/app" { owner = "root", group = "root", mode = "0644", content = [=[# This is generated by cofu. don't edit it manually! MAILTO="" */1 * * * * kohkimakimoto /path/to/app/batch1 */1 * * * * kohkimakimoto /path/to/app/batch2 */30 * * * * kohkimakimoto /path/to/app/batch3 0 * * * * kohkimakimoto /path/to/app/batch3 ]=], }
これをアプリのリポジトリルートにおいておき、デプロイ時のスクリプトにsudo -E cofu config_cron.lua
を実行するようにしてあります。
よって私の環境では実際に使っているリソースはtemplate
とservice
,execute
あたりのみになります。ただ、今後の個人の趣味プロジェクトなどではサーバプロビジョニングの用途でも使っていこうかな、とも思っています。せっかくその他のパッケージやユーザを管理するリソースも実装したので。。。
ちなみにレシピはシバンで使うこともできます。
https://ja.wikipedia.org/wiki/%E3%82%B7%E3%83%90%E3%83%B3_(Unix)
レシピを以下のように記述して、実行権限をつければ、レシピファイル自体を実行可能な設定スクリプトとして扱うことができます。
#!/usr/bin/env cofu -- config.lua template "/path/to/foo" { -- ... } template "/path/to/bar" { -- ... } -- 実行権限をつけて直接実行。 -- $ ./config.lua
実装のはなし
今回のCofuがそうなのですが、最近CLIツールを作るのにGoとGoPherLuaというGoのLua実装をセットで使うのが気に入っていて、これをベースにLuaをDSL風に使った設定ファイルを設計してアプリケーションを組み上げるのが最近、個人的によく使う手法になっています。
LuaのDSLはnginxの設定ファイルやHCLに見た目が似ていて、かつ変数や制御構文もつかえるので、なかなか便利だと思っています。
他にも幾つかこの技術スタックでツールを書いているので、後日それについても記事を書こうかと思っています。
CentOS6でサーバを構築する際におこなっている基本設定
2016年にもなって今さらという感じですが、CentOS6でサーバを構築する際の自分がやっている初期の基本設定、確認事項をまとめておこうと思います。
CentOS6のサポート期限が2020年までで、自分が今後メインに使っていく環境もCentOS7などに移行しようと思っています。遅まきながらローカルのVirtualBoxにCentOS7をいれていろいろ試していますが、CentOSは6と7で大幅な変更がなされ、今まで普通に出来ていたことを一から学び直す必要があったりしてツライ日々ではあります。
このように徐々にCentOS6から離れていくことになるので、そのあたりの記憶が急速に失われていく前に、今まで書いたメモなどを元に情報を整理しておこうかと思い立ったのがきっかけです。
※ここではサーバのロールにかかわらず行う基本的な作業をまとめています。通常この環境の上にWebサーバ(httpd, nginxなど)やDB(MySQLなど)をインストールすることになります。
※本文中のコマンドは基本的にrootユーザで行っているものとします。一般ユーザを使い必要に応じて都度root権限を使うようにしている方はsudo
をつけるなど、コマンドを読み替えてください。
- ランレベルの確認
- サービス起動設定の確認
- ディクスサイズの確認
- ロケールの設定
- タイムゾーンとNTPの設定
- ネットワーク関連の設定
- yumリポジトリ
- bash-completion
- ファイアウォール (iptables)
- sshd
- logrotate
- sysctl
- 環境変数
ランレベルの確認
OSのランレベルを確認します。ここではサーバ用途なので3が設定されていればいいでしょう
# cat /etc/inittab
# inittab is only used by upstart for the default runlevel. # # ADDING OTHER CONFIGURATION HERE WILL HAVE NO EFFECT ON YOUR SYSTEM. # # System initialization is started by /etc/init/rcS.conf # # Individual runlevels are started by /etc/init/rc.conf # # Ctrl-Alt-Delete is handled by /etc/init/control-alt-delete.conf # # Terminal gettys are handled by /etc/init/tty.conf and /etc/init/serial.conf, # with configuration in /etc/sysconfig/init. # # For information on how to write upstart event handlers, or how # upstart works, see init(5), init(8), and initctl(8). # # Default runlevel. The runlevels used are: # 0 - halt (Do NOT set initdefault to this) # 1 - Single user mode # 2 - Multiuser, without NFS (The same as 3, if you do not have networking) # 3 - Full multiuser mode # 4 - unused # 5 - X11 # 6 - reboot (Do NOT set initdefault to this) # id:3:initdefault:
id:3:initdefault:
となっていることを確認します。
サービス起動設定の確認
chkconfig
で起動設定になっているサービス(デーモン)を確認します。
# chkconfig --list auditd 0:off 1:off 2:on 3:on 4:on 5:on 6:off blk-availability 0:off 1:on 2:on 3:on 4:on 5:on 6:off cgconfig 0:off 1:off 2:off 3:off 4:off 5:off 6:off cgred 0:off 1:off 2:off 3:off 4:off 5:off 6:off crond 0:off 1:off 2:on 3:on 4:on 5:on 6:off ...
不要と思われるサービスはサーバリソースの無駄使いになるので、以下のコマンドで停止設定にしておきます。
# chkconfig <サービス名> off
これはOS起動時のサービスの自動起動設定なので、合わせて
# /etc/init.d/<サービス名> stop
として現在の実行状態も停止します。
さて、サーバ 用途でランレベルは3になっているので3:on
となっているサービスを上から順に調べて、offにしていくのですが、自分の知識範囲でよくわからないものもあったします。その場合はとりあえずonにしておいています。もちろんすべて把握できるのが良いのですが。。。
逆にOSの起動時に実行させたいサービスは
# chkconfig <サービス名> on
とします。
ディクスサイズの確認
# df -h
-h
をつけるとサイズにM(メガ)とかG(ギガ)とかがついてわかりやすく表示されます。
サーバスペックにあったディクスサイズになっていることや、パーティションを確認します。
ロケールの設定
システムが使うロケールを/etc/sysconfig/i18n
で設定します
日本語なら
LANG="ja_JP.UTF-8"
英語なら
LANG="en_US.UTF-8"
としておきます。また
# yum install man-pages-ja
をインストールして、ロケールを日本語にしておくとmanページが日本語表示されるようになります。
タイムゾーンとNTPの設定
タイムゾーンを日本にする
# cp -f /usr/share/zoneinfo/Japan /etc/localtime
/etc/sysconfig/clock
を以下のように編集する
ZONE="Asia/Tokyo"
サーバの時刻をあわせるntpdについての設定/etc/ntp.conf
を確認します。
server 0.centos.pool.ntp.org iburst server 1.centos.pool.ntp.org iburst server 2.centos.pool.ntp.org iburst server 3.centos.pool.ntp.org iburst
のようにntpサーバが指定されていること。
date
コマンドで表示される時刻が正しいことを確認する。
# date
ネットワーク関連の設定
幾つかあるので、順番に見ていきます。
ネットワークの設定
/etc/sysconfig/network
NETWORKING
がyes
になっているのとHOSTNAME
を確認する
NETWORKING=yes HOSTNAME=your-hostname
ネットワーク・インターフェースの確認
ネットワーク・インターフェースは/etc/sysconfig/network-scripts
配下にインターフェースごとにifcfg-xxx
というファイルに設定されている。
例えばifcfg-eth0
はイーサネットの0番目のインターフェース
DEVICE="eth0" BOOTPROTO="none" IPADDR=192.168.56.14 NETMASK=255.255.255.0 ONBOOT="yes" TYPE="Ethernet" UUID="c67412a6-19cf-427a-9f4d-96ca89217519"
静的はIPを割り当てている場合はIPADDR
やNETMASK
などの設定を確認、編集する。またネットワークの設定を変更したら
# /etc/init.d/network restart
で反映させる。
IPアドレスの確認
# ifconfig
IPアドレスの設定が、設定通りか確認します。
ネットワークの疎通確認
# ping yahoo.co.jp
pingが外に向けて通るかを確認します。0% packet loss
となればOK。
# traceroute yahoo.co.jp
外につなぎに行く際にどのようなIPを経由するかを確認。ネットワーク管理者ではないので、ざっと結果を眺めるだけです。
yumリポジトリ
yumはCentOSの標準パッケージ管理システムです。システムワイドにパッケージをインストールする場合、基本的にyumを使う方針にしています。ソースからのインストールに代表されるその他のインストール方法はファイルシステムのどこに何が配置されるか把握、管理するのが難しく、これはアンインストールなどの作業を困難にします。
ただCentOSでは標準のyumリポジトリでインストールできるパッケージは数が少なかったり、バージョンが古かったりする(特にPHPのバージョンが古いのがLAMP環境では致命的です)ので、以下のサードパーティのリポジトリを追加して使っています。俗に「野良リポジトリ」と呼ばれるものです。
- epel
- remi
追加は以下のコマンドを実行します。
# rpm -ivh http://ftp.riken.jp/Linux/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm # rpm -ivh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
追加したサードパーティリポジトリは標準では想定されていない環境をインストールするので、予期せぬ不具合を発生させる可能性があると思われます。デフォルトでは使わないように設定し、インストールやアップデート時、必要に応じて有効化して使うのが広く知られている手法のようです(※といいつつ、私はデフォルトで有効化してしまっています。後述します)。その場合は以下のように設定ファイルを修正、確認しましょう。
/etc/yum.repos.d/epel.repo
[epel] # 中略... enabled=0
/etc/yum.repos.d/epel.repo
[remi] # 中略... enabled=0
これでデフォルトは無効化されます。利用する場合は以下のようにします。
# yum install php --enablerepo=remi,epel
コマンドメモ
インストール(phpをインストールの例)
# yum install php
アップデート
# yum update php
アンインストール
# yum remove php
インストール済みパッケージの情報
# rpm -qi php
インストール済みパッケージのファイル一覧
# rpm -ql php
補足(個人的なデフォルトの設定)
前述したように、サードパーティリポジトリ利用のデフォルト設定を無効にはしていません。というのも実際にパッケージをインストールする作業はchefなどのプロビジョニングツールを使っているので、デフォルトで有効になっていないと、これらのツールから正しくインストールできないという理由があるからです。
bash-completion
素のbashのままだとzshになれた補完脳にはツライので、bash-completionを入れます。
# yum install bash-completion
インストールするだけで、ある程度補完が効くようになります。カスタマイズは特にしていません。
ファイアウォール (iptables)
iptablesはファイアウォールです。iptables
コマンドから設定できるのですが、よく覚えてないし全体の設定を眺めて設定できるので、設定ファイルを直接編集しています。(設定ファイルの先頭にManual customization of this file is not recommended.
って書かれているので本来は非推奨なのですが。。。)
iptablesの設定の詳細は覚えていないのですが(よくググります)、基本的に以下のようにINPUTチェインに外部接続を許可する設定を書いていきます。 最後にそれ以外はすべて拒否するように設定しておきます。
/etc/sysconfig/iptables
# Firewall configuration written by system-config-firewall # Manual customization of this file is not recommended. *filter :INPUT ACCEPT [0:0] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [0:0] -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT -A INPUT -p icmp -j ACCEPT -A INPUT -i lo -j ACCEPT # httpd -A INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT -A INPUT -m state --state NEW -m tcp -p tcp --dport 443 -j ACCEPT # ssh -A INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT # ...中略 # 最後に拒否 -A INPUT -j REJECT --reject-with icmp-host-prohibited
これで必要のない外部アクセスを遮断でき、不正アクセス対策になります。最後にrestartして設定を反映します。
# /etc/init.d/iptables restart
なお、iptablesの設定をミスってsshでリモートサーバに繋げなくなって詰む、という状況を避けたいので今のターミナルのセッションが生きているうちに、restart
したらまず別のターミナルを立ち上げてssh接続を試します。
sshd
ssh接続はセキュリティを強くするため、ポート番号を変えて、鍵認証オンリーにします。以下、編集および確認箇所のみを記載します。
/etc/ssh/sshd_config
# 22以外に変えます Port 2222 # rootログインは禁止 PermitRootLogin no # パスワード認証は禁止 PasswordAuthentication no
再起動して設定を反映します。
/etc/init.d/sshd restart
logrotate
ログのローテート設定。ログが無尽蔵に大きくなってディクスサイズを圧迫しないように設定、確認します。 初期設定時の他にhttpdなどのデーモンやログを出力するアプリケーションを追加した時にも目を通します。
/etc/logrotate.d/
以下にログの対象ごとに設定ファイルがあるのでそれを確認します。例えばnginxは以下のようになっていたりします。
/etc/logrotate.d/nginx
/var/log/nginx/*.log { daily missingok rotate 52 compress delaycompress notifempty create 640 nginx adm sharedscripts postrotate [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid` endscript }
実際の設定はログのサイズや保存期間の要件などによって異なるので、それを考慮し設定します。
sysctl
カーネルパラメータの設定です。/etc/sysctl.conf
を編集します。以下のパラメータを追加、変更。
net.nf_conntrack_max = 1000000 net.netfilter.nf_conntrack_max = 1000000 net.core.somaxconn = 1024 vm.overcommit_memory = 1
net.nf_conntrack_max
とnet.netfilter.nf_conntrack_max
ネットワークアクセスが高負荷になったときパケットのトラッキングがデフォルトの上限値を上回ることがあるのでそれに対応するため、実際の数値はメモリの量と相談です。
参考:
https://www.e-agency.co.jp/column/20121225.html
net.core.somaxconnは
ソケットをListenするキューの最大数。memcacnedとかredisはこれがカーネルのデフォルト値(128)より大きく設定されているので、増やす
参考:
net.core.somaxconnについて調べてみた - tetsuyai’s blog
vm.overcommit_memory
はメモリが足りなくなったときのmalloc
の動作についての制御のよう。デフォルトだと高負荷時にredis起動時にバックグランドセーブが失敗する可能性がある、という警告がでる。
参考:
passingloop • Linux のオーバーコミットについて調べてみた
設定値を反映するために以下のコマンドを打つ。
sysctl -p
確認。
sysctl -a
環境変数
環境変数はサーバにデプロイするアプリケーションの振る舞いを制御するためなどに使います。
サーバのロールやdevelopment
やproduction
のような環境情報を設定しておきます。
アプリケーションは実行時にこの環境変数を読むことでデバッグモードに変えたり、本番用の設定に切り替えたりできるように実装します。
ただ環境変数はsudo時、sshでログイン時、sshで直接コマンド実行時、実行中のhttpdプロセス内、などプロセスの実行のコンテキストによって変わるので、 常に一貫した設定を読めるようにしておくのがなかなか難しく、悩みどころになっています。現状は同じ環境変数を反映させる設定を複数の箇所に行っています。
/etc/envrionment
SERVER_ENVIRONMENT=production SERVER_ROLE=web
/etc/profile.d/environment.sh
export SERVER_ENVIRONMENT=production export SERVER_ROLE=web
以上2つが基本。あとはhttpdをアプリケーションサーバに使うときは/etc/httpd/conf/httpd.conf
にも設定します
SetEnv SERVER_ENVIRONMENT "production" SetEnv SERVER_ROLE "web"
設定の記述が重複してしまうのでコピペにならないように、chefやitamaeのようなプロビジョニングツールで変数にしてカバーしています。。。