技術メモなど

ほぼ自分用の技術メモです。

Squidがアクセス集中時に遅くなったときに行った対策

始業時や昼休みなど、アクセスが集中する時間帯になるとLinuxSquidプロキシサーバのレスポンスが悪くなって、「Webアクセスが遅い!!」とあちこちから苦情を言われてしまっていたのですが、次のような対策を行ったところ、無事解決できたっぽいのでメモることにします。

現状の確認

CPUやメモリの使用率を確認

まずはお決まりのtopコマンドをアクセス集中時に実行(topコマンド自体が高負荷時にはサーバに更に追い打ちをかけそうですが、原因の切り分けのためにはやむを得ないと思います)。やはりsquidプロセスが一番CPUもメモリも喰っていましたが、load averageは低く、CPUやメモリに負荷がかかっているようには見えない状況でした。

ネットワークの状況を確認

次に同じくアクセス集中時にnetstatコマンドを実行。すると状態がESTABLISHEDのコネクションよりもTIME_WAITやSYN_RECVのものが多く、ネットワークの待ち行列ボトルネックになっている状況が見えてきました。

ネットワーク周りのカーネルパラメータのチューニング

そこで、カーネル周りについて、次のようなチューニングを実施しました。基本線は、OSで受け付けられるコネクションを増やし、不要になったクライアントのコネクションを早く次のクライアントのコネクションに回せるようにします。

なお、全て /etc/sysctl.conf に追記し、sysctl -pで設定を反映させています。

tcp_fin_timeout

ソケットを強制的にクローズする前に、 最後の FIN パケットを待つ時間を秒単位で指定する。 Man page of TCP

tcp_fin_timeoutを短くすれば、通信が終わって不要になったコネクションを解放する時間も早くなるのでは?と考え、30秒に設定(元は60秒でした)。

net.ipv4.tcp_fin_timeout = 30

tcp_max_syn_backlog

接続してきているクライアントから ack を受信していない状態の接続リクエストをキューに置ける最大数。 この数値を越えると、カーネルはリクエストを捨て始める。

Man page of TCP

この値が少ないと、アクセス集中時に接続してきたクライアントのコネクションを切ってしまう→クライアントのリクエストはタイムアウト、となっていたと思われるので、元の設定(1024でした)よりも大きめに設定。

net.ipv4.tcp_max_syn_backlog = 4096

somaxconn

TCP ソケットでの backlog 引き数の振る舞いは Linux 2.2 で変更された。 現在ではこの引き数は、 受け付けられるのを待っている、 完全に 確立されたソケットのキューの長さを指定する。 以前は不完全な接続要求の数であったが、これを置き換えた。 不完全なソケットのキューの最大長は /proc/sys/net/ipv4/tcp_max_syn_backlog を用いて設定できる。 syncookie が有効になっている場合、 論理的な最大長は存在せず、この設定は無視される。

backlog 引き数が /proc/sys/net/core/somaxconn の値よりも大きければ、 backlog の値は暗黙のうちにこの値に切り詰められる。 このファイルのデフォルト値は 128 である。 バージョン 2.4.5 以前のカーネルでは、この上限値は コード埋め込みの固定値 SOMAXCONN であり、その値は 128 であった。

Man page of LISTEN

tcp_max_syn_backlogの値を上げても、somaxconnの値が低ければこちらに引っ張られてしまうとのこと(参考→DSAS開発者の部屋:高負荷サイトのボトルネックを見つけるには)。そこでsomaxconnもtcp_max_syn_backlogと同じ値に設定。

net.core.somaxconn = 4096

netdev_max_backlog

Sets the maximum number of packets allowed to queue when a particular interface receives packets faster than the kernel can process them. The default value for this file is 300.

3.3.9.4. /proc/sys/net/

こちらもバックログ関係の設定。同じく4096に設定。

net.core.netdev_max_backlog = 4096

tcp_max_tw_buckets

システムが許容する TIME_WAIT 状態にあるソケットの最大数。 この制限が存在するのは、 単純な使用不能 (denial-of-service) 攻撃を防ぐために過ぎない。 デフォルト値は NR_FILE*2 で、システムのメモリに応じて調整される。 この数値を越えると、そのようなソケットはクローズされ、警告が表示される。

Man page of TCP

TIME_WAITで待てる数を増やすため、元よりも大きな値に設定。

net.ipv4.tcp_max_tw_buckets = 360000

tcp_tw_reuse

プロトコルの面から見て問題ない場合に新規コネクションに TIME_WAIT 状態のソケットを再利用することを許可する。

Man page of TCP

待ちコネクションの回転率を上げるために有効に設定。

net.ipv4.tcp_tw_reuse = 1

カーネルチューニングの効果

再びアクセス集中時にnetstatコマンドを実行。すると状態がTIME_WAITやSYN_RECVのコネクションが格段に減少しました。チューニングの効果は確かにありました。これでSquidのパフォーマンスも上がるはず……が、ほとんどWebアクセスのレスポンスは変わりませんでした。

再び現状の調査

OSの資源は枯渇していない、ネットワークのボトルネックも解消した、それでもSquidが遅いとなると、Squid自身の問題を疑うしかありません。access.logを眺めても異常は見つからず。悩みながらふとcache.logを見てみると、Your cache is running out of filedescriptorsという見慣れないログが。grepすると出るわ出るわ……それもちょうどアクセスが集中する時間帯にばかり多発しているようでした。これで原因はSquidキャッシュのオープンがファイルディスクリプタの上限を超えてしまっていることに絞られました。

Squidのファイルディスクリプタの上限値を変更

squidのコンフィグ変更

/etc/squid/squid.conf のmax_filedescの値を4096に増やし、コメントアウトを解除します(元の設定にない場合は追記)。

$ sudo sed -i -e 's/^# max_filedesc 1024/max_filedesc 4096/' /etc/squid/squid.conf

もしかしたら次項のulimitの設定だけでいいのかも。ドキュメントにはulimitの設定を引き継ぐならこの設定を消しなさいと書いてあるように読めます。

Reduce the maximum number of filedescriptors supported below the usual operating system defaults.

Remove from squid.conf to inherit the current ulimit setting.

squid : max_filedescriptors configuration directive

Squid実行ユーザーの上限値変更

rootや一般ユーザーとは違い、デーモンを実行するユーザー(squidapacheなど)は、/etc/security/limits.conf に設定してもulimitの設定は反映されないそうです(知らなかった)。

ファイルディスクリプタ数の上限変更とlimits.confの罠 (ゆめ技:ゆめみスタッフブログ)に詳しく解説されていました。以下引用します。

daemon系プロセスのファイルディスクリプタ数上限を設定する際、/etc/security/limits.conf は使えません。状況によっては一見設定されたように見えますが、大きな落とし穴にはまることになります。

面倒ですが、必要なプロセス毎にulimitを用いて適切に設定しましょう。

ファイルディスクリプタ数の上限変更とlimits.confの罠 (ゆめ技:ゆめみスタッフブログ)

そこで、squidの起動スクリプト /etc/rc.d/init.d/squid に次を追記します。これにより、Squidの実行ユーザー(squid)がスクリプト実行した際にulimitの設定が行われます。

ulimit -HSn 4096

追記後はSquidを再起動すれば設定が反映されます。

結果

無事にアクセスが集中する時間帯でもファイルディスクリプタのエラーは起きなくなったみたいです。Webアクセスのレスポンスも一気に改善しました。

反省点は、切り分けの順番として先にSquidの全部のログを調べるべきだったなというところです。いずれにしてもネットワーク周りのチューニングも必要だったでしょうけど。

opscode-cookbooks/vimのコミュニティクックブックでVim 7.4をインストールしようとしてハマったメモ

chefが提供している、vimをインストールするコミュニティクックブックを使って、Vagrant上のCentOS 6.6にchefでVim 7.4をソースからインストールしようとしたらハマってしまったので、メモしときます。

まずBerksfileを次のとおり編集します。

source "https://supermarket.chef.io"
(中略)
cookbook ['vim']

コミュニティクックブックをberks vendorコマンドでインポートします。

$ bundle exec berks vendor ./cookbooks

この状態でvagrant provsionすると、yumvim-enhancedが正常にインストールされます。しかし、CentOS 6のyumでインストールするVimRubyに対応していないので、ソースからコンパイルしてVim 7.4をインストールしようと思いました。

インポートしたコミュニティクックブックのREADME.mdを読むと次のように書いてあります。

If you would rather compile vim from source, as the case may be for centos nodes, then override the node['vim']['install_method'] with a value of 'source'.

そこで./cookbooks/vim/attributes/default.rbを次のように変更します。

(省略)
default['vim']['install_method'] = 'source'

そしてvagrant provisionを実行すると、次のようなエラーが出力されてプロビジョンが失敗します。

==> default: [2014-12-07T03:29:33+09:00] FATAL: Stacktrace dumped to /var/chef/cache/chef-stacktrace.out
==> default: [2014-12-07T03:29:33+09:00] ERROR: remote_file[/var/chef/cache/vim-7.4.tar.bz2] (vim::source line 29) had an error: Chef::Exceptions::ChecksumMismatch: Checksum on resource (607e13) does not match checksum on content (d0f5a6)
==> default: [2014-12-07T03:29:33+09:00] FATAL: Chef::Exceptions::ChildConvergeError: Chef run process exited unsuccessfully (exit code 1)

チェックサムが合わないとのことなので、Vimの配布元でMD5SUMの値を確認します。

607e135c559be642f210094ad023dc65 vim-7.4.tar.bz2

./cookbooks/vim/recipes/source.rbの内容を確認します。

(省略)
remote_file "#{cache_path}/vim-#{source_version}.tar.bz2" do
  source "http://ftp.vim.org/pub/vim/unix/vim-#{source_version}.tar.bz2"
  checksum node['vim']['source']['checksum']
  notifies :run, "bash[install_vim]", :immediately
end
(省略)

このレシピではVimのソースファイルのチェックサムを確認するようになっています。ということはダウンロードしようとするファイルのチェックサムと、レシピに記載したチェックサム値が合っていないからプロビジョンに失敗したと思われます。そこで、./cookbooks/vim/attributes/source.rbの内容と比較してみます。

(省略)

default['vim']['source']['checksum']      = '607e135c559be642f210094ad023dc65'
(省略)

合っている……。

しばらく悩みながら調べてみると、やっと原因が分かりました。

chefのドキュメントを読むと、remote_fileのchecksumはMD5ではなく、sha256で比較しているとのことでした。

checksum Optional, see use_conditional_get. The SHA-256 checksum of the file. Use to prevent the remote_file resource from re-downloading a file. When the local file matches the checksum, the chef-client will not download it.

そりゃエラーになるわけです。Vimの配布元にはsha256の値は公開されていないため、自分で調べる必要があります。

$ wget ftp://ftp.vim.org/pub/vim/unix/vim-7.4.tar.bz2
$ shasum -a 256 vim-7.4.tar.bz2
d0f5a6d2c439f02d97fa21bd9121f4c5abb1f6cd8b5a79d3ca82867495734ade

./cookbooks/vim/attributes/source.rbのdefault['vim']['source']['checksum']の値をsha256の値に変更します。

$ sed -i -e 's/607e135c559be642f210094ad023dc65/d0f5a6d2c439f02d97fa21bd9121f4c5abb1f6cd8b5a79d3ca82867495734ade/' ./cookbooks/vim/attributes/source.rb

これでvagrant provisionが成功するようになります。

しかしなぜchefの公開しているレシピなのにdefault['vim']['source']['checksum']にMD5の値を書いているのだろう。もしかしてMD5でチェックするオプションがあるんでしょうか?

タグが<none>になってしまったdocker imageをまとめて削除するワンライナー

よくDockerfileの中身を精査しないままdocker buildをして失敗する私のような人間は、docker imagesしたらこんな状態になっていると思います。

REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
takedah             postfix             684aa52fc89e        24 hours ago        515.2 MB
takedah             rails               66bf45ac2ac2        11 weeks ago        1.241 GB
<none>              <none>              c97d7e856595        11 weeks ago        1.233 GB
<none>              <none>              31f14a3de2ae        11 weeks ago        1.233 GB
<none>              <none>              f22584c8eaa8        11 weeks ago        1.233 GB
<none>              <none>              0f843867c4f6        11 weeks ago        1.063 GB

このタグが<none>になっているイメージをまとめて消すワンライナーです(実行は自己責任で!)。

$ sudo docker images | awk '{if ($2 ~ "<none>") print $3}' | xargs sudo docker rmi

docker imagesコマンドの標準出力をawkに渡して、第2フィールド(TAG)が<none>であれば、第3フィールド(IMAGE ID)を出力し、最後にxargsに渡してdocker rmiコマンドの引数にする、という仕掛けです。

って書いて、先日のDocker勉強会@旭川で@smaguchiさんが既にやり方を@SamuraiT01くんのGistに書いていたことに気付いた。

しつこくSMTP Authを試みてくるIP Addressをiptablesでブロックする

インターネットからアクセスできるSMTPサーバを立て、自ドメイン以外のメールリレーを全て拒否した上でSMTP Authを有効にして管理しているのですが、定期的に外部から何度もSMTP Authを試されてしまっています。

/var/log/maillogには次のようなログが繰り返し記録されています。

Sep 17 15:35:32 hoge postfix/smtpd[22321]: connect from fuga.example.com[xxx.xxx.xxx.xxx]
Sep 17 15:35:34 hoge postfix/smtpd[22321]: warning: SASL authentication failure: Password verification failed
Sep 17 15:35:34 hoge postfix/smtpd[22321]: warning: fuga.example.com[xxx.xxx.xxx.xxx]: SASL PLAIN authentic
ation failed: authentication failure
Sep 17 15:35:37 hoge postfix/smtpd[22321]: warning: fuga.example.com[xxx.xxx.xxx.xxx]: SASL LOGIN authentic
ation failed: authentication failure
Sep 17 15:35:37 hoge postfix/smtpd[22321]: disconnect from fuga.example.com[xxx.xxx.xxx.xxx]

自分しか使わないSMTPサーバなので、複数回SMTP Authに失敗しているイコール不正中継の踏み台にしようとしているのは明らかな状況です。

大変気持ちが悪いので、一定回数以上SMTP Authに失敗しているIP Addressを/var/log/maillogから抽出し、当該IP AddressをiptablesでDROPしてしまおうと考えました。

maillogから一定回数以上認証失敗しているIPを抽出するワンライナー

まずは/var/log/maillogから一定回数以上SMTP Authに失敗しているIP Addressを抽出できるか試してみます。

# cat /var/log/maillog | grep 'authentication failed' | egrep -o -e '\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\]' | sed -e 's/^\[\(.*\)\]$/\1/g' | sort | uniq -c | awk '$1 > 5 {print $2}'

上記ワンライナーで、/var/log/maillogから「authentication failed」の行の中からIPv4 Addressを取り出し、重複カウントした上で重複カウント数が5を超えるIPv4 Addressのみを抽出できました。

一応解説します。

egrep -o -e '\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\]' | sed -e 's/^\[\(.*\)\]$/\1/g'

egrepの-oオプションで、パターンにマッチした文字列のみを取り出します。-eオプションでパターンに正規表現を使ってIPv4 Addressっぽい文字列(数字1桁から3桁までの固まりがピリオド区切りで4つ連続している文字列)を取り出しています。

スマートではないのですが、maillogのauthentication failedログの行には[xxx.xxx.xxx.xxx]という形でIP Addressが書かれているため、まずこの部分を抽出してから更にsedにパイプして正規表現[]を取り除いています。ただ、万一[999.256.1.254].netのようなホスト名がログに記録されていればマッチしてしまうので、もう少しちゃんと書いた方がいいかもしれません。

| sort | uniq -c | awk '$1 > 5 {print $2}'

sortした上でuniqに-cオプションを付けて渡せば、重複している行をまとめて重複したカウント数も教えてくれます。次のような出力が返ります。

      2 xxx.xxx.xxx.xxx
     10 yyy.yyy.yyy.yyy
      6 zzz.zzz.zzz.zzz

一行がカウント数+半角スペース+IP Addressという形で返るため、更にこれをawkに渡してやれば、カウント数とIP Addressを分けて変数に格納できるため、最後はawkでカウント数が5以上であればIP Addressを表示する、というスクリプトになっています。

最終的に一行にカウント数5以上のIP Addressという形式の標準出力を得られました。

これを後述するiptablesから読み込むため、標準出力を任意の場所にテキストファイルとして出力しておきます。

# cat /var/log/maillog | grep 'authentication failed' | egrep -o -e '\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\]' | sed -e 's/^\[\(.*\)\]$/\1/g' | sort | uniq -c | awk '$1 > 5 {print $2}' >> /etc/postfix/sasl_failed_ip.txt

iptablesにファイルから読み込んだIP Addressをブロックする処理

以下は完全におまけです。シェルスクリプトに、上記手順で作成したテキストファイルを一行ずつ読み込んで、iptablesコマンドを生成する処理を作成します。

#!/bin/bash
while read LINE
  do
    iptables -A INPUT -s $LINE -j LOG --log-prefix "[Iptables DROP_SASL_FAIL]" || exit 1
    iptables -A INPUT -s $LINE -j DROP
  done < /etc/postfix/sasl_failed_ip.txt

一応ログプレフィクスを付けてログを出力するようにしています。

最終形のシェルスクリプト

一連の処理をシェルスクリプトにまとめるとこんな感じでしょうか。

#!/bin/bash
# Deny SASL Auth failed ip address
cat /var/log/maillog | grep 'authentication failed' | egrep -o -e '\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\]' | sed -e 's/^\[\(.*\)\]$/\1/g' | sort | uniq -c | awk '$1 > 5 {print $2}' >> /etc/postfix/sasl_failed_ip.txt || exit 1
while read LINE
  do
    iptables -A INPUT -s $LINE -j LOG --log-prefix "[Iptables DROP_SASL_FAIL]" || exit 1
    iptables -A INPUT -s $LINE -j DROP
  done < /etc/postfix/sasl_failed_ip.txt
exit $?

注意点は繰り返しになりますが、万一[999.256.1.254].netのようなホスト名だった場合、iptablesの処理で必ずエラーになるので、正規表現を練り直す必要があるかもしれません。

ちなみに私は自分のSMTPサーバにはIPv6でしかアクセスしないため、IPv4SMTP Authに失敗している時点で問答無用で弾いてしまってもいい状況です。

今のところIPv6でこの手の探索行為は受けている形跡はなさそうですが、もしIPv6の対策も考えなければならなくなったとき、正規表現がめんどくさそう……という気がします。

さくらのVPSとRTX1100をopenswanでipv6 over ipv6 ipsecを張ってみる

さくらのVPS上のCentOS 6.5と、自宅のYAMAHA RTX1100との間の通信をipsecで暗号化したいと思い立ちました。

さくらのVPSは固定グローバルipv4アドレスが一つ付与されているため、自宅側にも固定グローバルipv4アドレスがあれば話が早いのですが、あいにくこれがありません。しかし、自宅には半固定ipv6グローバルアドレスが割り当てられる環境になっており、さくらのVPSも固定グローバルipv6アドレス(Global Unicast Address、GUA)が付与されているため、ipv6ipsecを張ることにします。

色々調べた結果、openswanでipv6 ipsecができるようだったので、CentOS 6.5にはopenswanをインストールします。

※個人的に手探り状態で設定したので、正しくない箇所があるかもしれません。もし参考にする方がいれば、十分検証のうえでお願いします。

事前共有キーの設定

openswanで事前共有キー(PSK)方式のipsecを張ります。事前共有キーは十分に複雑で長い文字列を設定しましょう。

RTX1100の設定

WAN側インターフェイスへGUAの付与

自宅はひかり電話ありフレッツ光ネクストに、IIJmio FiberAccess/NFで、IPoE方式でipv6を使える環境です。ひかり電話ありの場合、ひかり電話ルータにRTXを繋げば、DHCPv6-PDでprefix移譲を受けられます。

余談ですがIIJmio FiberAccess/NFの場合、RTXには/60のprefixが移譲されてきたため、64bit-60bit=4bit分、2の4乗分サブネットをRTX1100で切ることができます。うちではRTX1100でVLAN切って各VLANにGUAを付与し、自宅サーバKVMにトランク接続しているので、KVM仮想マシンにはグローバルのipv6サブネットを10以上割り当てられる状態になっています。

あとはGUAをRTXのWAN側インターフェイスに割り当てる設定が必要です。具体的にはYAMAHAのサイトに設定例が出ているので(フレッツ 光ネクストでインターネット接続(IPv6 IPoE)を使用するには、どうすればよいですか? )、この通り設定すれば大丈夫ですが、RTX1100のファームウェアが古い場合、設定例通りにするとひかり電話ルータにRTX1100を繋いだ途端にRTX1100が再起動を繰り返してしまう不具合があるため、必ずRev. 8.03.94以降のファームウェアを適用してから設定しましょう。

LAN側インターフェイスへULA(Unique Local IPv6 Unicast Addresses)の付与

今回ipsecトンネルを通すipv6アドレスは、RTX側もCentOS側も、ipv4でいうところのプライベートIPアドレスであるULAのipv6アドレスにします。

ULAは他のipv6アドレスと重複しないよう、基本的にインターフェイスMacアドレスを基に計算して生成します。ULAの生成Webサービスを使うと楽です(Generate Unique Local Address)。

RTX側、CentOS側のいずれのULAにも外部から到達できませんが、今回RTX側、CentOS側にそれぞれ互いのULAのサブネットに対してipsecトンネル経由でルーティングするよう設定すれば、ULA同士で通信できるという仕組みです。

RTXのLAN側インターフェイスに、生成したULAを付与します。ちなみにRTX1100は一つのインターフェイス複数ipv6アドレスを振れるので、各インターフェイスにはGUAとULAをそれぞれ付与することも可能です。

ipsecの設定

コンフィグの該当部分のみ抜粋します。なお、ip filterを設定する場合は、WAN側インターフェイスでUDP546(DHCPv6-PD)、esp、UDP500(ipsec)辺りのポートが開いている必要があるかと思います。

# 対向(CentOS側)ULAのサブネットへのルーティング追加
ipv6 route (CentOS側ULAサブネット) gateway tunnel 1
# ipsecトンネルの設定
tunnel select 1
 ipsec tunnel 1
  ipsec sa policy 1 1 esp 3des-cbc sha-hmac
  ipsec ike encryption 1 3des-cbc
  ipsec ike esp-encapsulation 1 off
  ipsec ike group 1 modp1024
  ipsec ike hash 1 sha
  ipsec ike keepalive use 1 on dpd
  ipsec ike local address 1 (RTXのGUA)
  ipsec ike log 1 key-info message-info payload-info
  ipsec ike payload type 1 3
  ipsec ike pre-shared-key 1 (事前共有キー)
  ipsec ike remote address 1 (CentOSのGUA)
  ipsec auto refresh 1 on
 ip tunnel tcp mss limit auto
 tunnel enable 1

CentOS 6.5の設定

eth1インターフェイスの追加

CentOSのeth0にグローバルipv6アドレスが付与されているので、eth1を作ってこれにULAを振ることにします。最初eth0にセカンダリipv6アドレスとしてULAを振って試したんですが、うまく行きませんでした。

eth1を追加するため、VPS同士をローカル接続させる仮想スイッチの仕組みを使います。コントロールパネルにて仮想スイッチを作成し、これにeth1を接続するよう設定すれば、さくらのVPS上のCentOSでeth1インターフェイスが活性化しますので、後はこれにULAを設定するだけです。

openswanのインストール

CentOS標準リポジトリからyumでインストールできます。

$ sudo yum install openswan

対向(RTX側ULAのサブネットへのルーティングテーブル追加)

ここが一番嵌りました。ipv6の静的ルーティングテーブル追加はipv4のようにコマンドで設定できません。

/etc/sysconfig/network-scripts/route6-eth1ファイルを作成し、そこに経路情報を記載します。

$ sudo echo '(RTXのULAのサブネット) via (CentOSのULA)' > /etc/sysconfig/network-scripts/route6-eth1

openswanの設定

基本的に以下の3つの設定ファイルの変更が必要です。

/etc/sysctl.conf

openswanを起動して設定を確認すると、カーネルパラメータを修正するようメッセージが表示されます。

$ sudo service ipsec start
$ sudo ipsec verify

そこで、次の通り追記します(たぶんipv6しか使わないなら関係ない設定だとは思います)。

$ sudo echo 'net.ipv4.conf.all.send_redirects = 0' >> /etc/sysctl.conf
$ sudo echo 'net.ipv4.conf.default.send_redirects = 0' >> /etc/sysctl.conf
$ sudo echo 'net.ipv4.conf.eth0.send_redirects = 0' >> /etc/sysctl.conf
$ sudo echo 'net.ipv4.conf.lo.send_redirects = 0' >> /etc/sysctl.conf
$ sudo echo 'net.ipv4.conf.all.accept_redirects = 0' >> /etc/sysctl.conf
$ sudo echo 'net.ipv4.conf.default.accept_redirects = 0' >> /etc/sysctl.conf
$ sudo echo 'net.ipv4.conf.eth0.accept_redirects = 0' >> /etc/sysctl.conf
$ sudo echo 'net.ipv4.conf.lo.accept_redirects = 0' >> /etc/sysctl.conf

また、ipv6パケットのフォワーディングができるように次の通り追記します。

$ sudo echo 'net.ipv6.conf.all.forwarding = 1' >> /etc/sysctl.conf

/etc/ipsec.d/connection.conf

ipsec設定の核の部分です。コンフィグはOpenswan/ipv6.conf.in at master · xelerance/Openswanを参考にしました。

conn rtx1100
        connaddrfamily=ipv6
        type=tunnel
        authby=secret
        auth=esp
        ike=3des-sha1;modp1024
        esp=3des-sha1;modp1024
        keyexchange=ike
        pfs=yes
        left=(CentOSのGUA)
        leftid=(同上)
        leftsubnet=(CentOSのULAのサブネット)
        leftsourceip=(CentOSのULA)
        right=(RTXのGUA)
        rightid=(同上)
        rightsubnet=(RTXのULAのサブネット)
        rightsourceip=(RTXのULA)
        ikelifetime=8h
        keylife=1h
        compress=no
        dpddelay=5
        dpdtimeout=20
        dpdaction=restart
        auto=start

/etc/ipsec.d/rtx1100.secrets

事前共有キーとなる文字列を記載しておくファイルです。

(CentOSのGUA) (RTXのGUA): PSK "(事前共有キー)"

openswanの起動

以上で、openswanを起動するとipv6 over ipv6 ipsecを張ってくれるはずです。

$ sudo service ipsec start

システム起動時に自動実行させておきます。

$ sudo chkconfig ipsec on

余談

RTX1100はipv4 over ipv6ipsecをサポートしているので、頑張ればipv4で通信できると思ったのですが、力及ばず駄目でした。

ipv6 over ipv6でのipsecする需要がどのくらいあるか分かりませんが、私はさくらのVPS上のCentOSでzabbix、rsyslogを動かしているので、これに使う通信経路が暗号化できて満足しています。

ちなみにさくらのVPS側でDNSも動かしているのですが、RTX1100自身からのDNSクエリについて送信元IPを指定できないため(ipv4ならできるのに!)、DNSクエリのパケットがipsecトンネルの方へ向いてくれません。syslogは送信元IPを指定できるのでipsecトンネルを経由してさくらのVPS側へ届くんですけどね……。YAMAHAさんがファームウェア更新で対応してくれないかななんて虫のいいことを考えています。

Markdown記法が使えるpukiwikiをインストールするDockerfile

タイトルのとおりです。ちょっとしたメモツールが欲しくて作りました。

基本的にLAN内で使う想定なので、セキュリティ対策が全くされていません。もし参考にする方がいれば、適宜セキュリティ対策をお願いします。

DockerfileはGithub/takedahに上げています。

CentOS6.5でDockerを動かしてみる(2)

Dockerイメージのビルドはもちろん手作業でやってもいいのですが、Dockerfileを使うのが良いと思いますので、ここからDockerfileを使ったDockerイメージのビルドについて説明します。

Dockerfileとは

Dockderfileとは、元となるイメージをプルして、ソフトウェアのインストールやコンフィグの変更を行うなどの一連の操作をファイル(Dockerfile)に順番に記載しておき、ファイルを元にイメージのビルドを自動実行するための仕組みです。これは次のようなメリットがあります。

  • 事前に構築手順を確認できるので、手作業でのビルドよりもミスを減らせる。
  • Dockerfile自体が引き継ぎドキュメントになり得る。
  • 異なるDockerホストで同一のDockerfileを使ったイメージのビルドを行えば、同一のイメージが出来上がる。

Dockerfileに書くこと

私のようにDockerfileを使ってとりあえずイメージのビルドをしてみようというレベルで必要となるDockerfileの記載事項を説明してみます。

FROM

元となるイメージを指定します。

RUN

実行する処理を指定します。これは複数書けるので、RUN yum -y updateして次にRUN yum -y install openssh-serverして、……といったように、Dockerfileのメインとなる部分です。

EXPOSE

コンテナに外部ネットワークからアクセスさせるときに使用するポートを記載します。ただ、この記載、本当に必要か分かりません。ここでついでにコンテナのネットワークの扱いについて説明します。

Dockerコンテナのネットワーク

Dockerコンテナにはそれぞれ仮想のネットワークインターフェイスがありますが、実体はホストOSに仮想インターフェイスが作られ、これがコンテナ用に割り当てられているようです。そのため、仮想インターフェイスからホストOSの実インターフェイスにNAPTすることで、コンテナが外部ネットワークとやりとりできる仕組みです。

ホストOSのルーティングテーブルを見てあげると、コンテナ用仮想インターフェイスと経路情報が分かります。

$ route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
10.1.0.0        *               255.255.255.0   U     0      0        0 eth0
link-local      *               255.255.0.0     U     1002   0        0 eth0
172.17.0.0      *               255.255.0.0     U     0      0        0 docker0
default         10.1.0.254      0.0.0.0         UG    0      0        0 eth0

上記の例では仮想インターフェイス名がdocker0で、172.17.0.0/16がコンテナに割り当てられるネットワークとなっています。

NAPTを使っているので、当然ホストOSで仮想インターフェイスからの送信パケット、仮想インターフェイスへの受信パケットについて、転送(FORWARD)を許可してあげないと、コンテナが外部ネットワークと通信できずに嵌ります。ホストOSのiptablesの設定は忘れずに。

ではNAPTでホストOSに届いたパケットをコンテナが受け取れるようにするにはどうすればいいかというと、docker runするときに次の通り引数を与えます。

$ docker run -i -t -p 10022:22 centos:centos6 /bin/bash

これでホストOSのTCP10022番宛てのパケットは、コンテナのTCP22番ポートへ転送されます。

ここで話が戻るのですが、DockerfileにEXPOSEで10022:22のような書き方が出来ません。そのうえ、EXPOSEに書かないでビルドしたイメージでも、上記のようにdocker runするときにポートを指定してあげればちゃんとパケットは転送されます。ですので、あまりEXPOSEの記載には意味がないのではと思っています。

CMD

docker runしたときに必ず実行させたいプロセスがあれば、CMDに記載します。例えばCMD ["/usr/sbin/httpd -D FOREGROUND]と記載すれば、docker runしたときに何も指定しなくてもapacheをフォアグラウンドで実行させることができます。

Dockerfileを用いたイメージのビルド

上記の内容でDockerfileというファイル名でテキストファイルを任意のディレクトリに作り、これを指定してdocker buildコマンドを実行します。

$ docker build -t takedah:test2 .

最後のピリオドがDockerfileを指します。カレントディレクトリにないDockerfileからビルドするなら、ピリオドの前にパスを書いてください。

これで作ったDockerfileを基にしたイメージが出来上がります。とても手軽です。

Dockerfile嵌りどころのチェックポイント

今回私が勉強会用に作ったDockerfileは、GitHubにアップしました。このDockerfileは、sshdRuby on Railsをセットアップし、sshdはsupervisorで実行するようにしたイメージをビルドするものです。

これを基に以下嵌ってしまったポイントを説明してみます。

yumリポジトリは固定した方が早い

ビルドの際にyumを使う場合、デフォルトではyumfastestmirrorプラグインが近いミラーサイトを探すと思いますが、Dockerイメージでyumをすると毎回決まって海外鯖に繋ぎにいくような気がします。そのため、yumリポジトリは国内鯖に固定した方が無難な気がします。以下リポジトリIIJに固定する例。

RUN sed -i -e 's/^#baseurl/baseurl/' /etc/yum.repos.d/CentOS-Base.repo
RUN sed -i -e 's/mirror\.centos\.org/ftp\.iij\.ad\.jp\/pub\/linux/' /etc/yum.repos.d/CentOS-Base.repo
RUN sed -i -e 's/^mirrorlist/#mirrorlist/' /etc/yum.repos.d/CentOS-Base.repo

Dockerコンテナで複数プロセスを実行するには

基本的にDockerコンテナは起動時に1プロセスしか実行できないため、supervisorからデーモンとしてプロセスを複数実行させる必要があります。以下supervisorをyumでインストールし、sshdをsupervisorから実行させる設定。

RUN yum -y --enablerepo=epel install supervisor
RUN sed -i -e 's/^nodaemon=false/nodaemon=true/' /etc/supervisord.conf
RUN echo '[program:sshd]' >> /etc/supervisord.conf
RUN echo 'command=/usr/sbin/sshd -D' >> /etc/supervisord.conf

sshでログインできない

設定合ってるのにログインできなくて悩んでたら、解決方法がありました。/etc/pam.d/sshdの設定変更が必要です。

RUN sed -i -e 's/^session *required *pam_loginuid\.so/session optional pam_loginuid/' /etc/pam.d/sshd

openssl-develがcentosplusリポジトリにある

標準リポジトリにopenssl-develがなくcentosplusリポジトリにあるため、リポジトリを指定してインストールする必要があります。

RUN yum --enablerepo=centosplus -y install openssl-devel

Dockerfileの行が変わると環境変数の設定が初期化されちゃう

例えばビルド中に/etc/profileの内容を反映させるとします。しかしDockerfileの次の行に処理が移ると、これが初期化されてしまいます。そのため、RUN source /etc/profile && 次のコマンド、というように、一行にまとめないとうまくいきません。

最後に

上記のDockerfileでビルドすれば、sshdRuby on Railsが動くCentOS6コンテナができますが、なぜかwebrickを起動するときにexecJSのライブラリが無い的なエラーが出て起動してくれません。

解決策は次の通りです。

  1. rails newしてプロジェクトを作成した後、Gemfileに"gem 'therubyracer'"を追記する。
  2. bundle installする。
  3. rails serverすればwebrickがちゃんと起動します。

勉強会では、上記のDockerfileからコンテナをビルドして実行し、railswebrickを起動してブラウザに表示させるところをデモとして見せたかったのですが、ど緊張して解決策の2番目を忘れてしまい、うまく行きませんでした……。

とりあえず勉強会で発表した内容をブログにまとめてみました。本当はこんなことを伝えたかったんだなと思っていただければ幸いです……。

今回主催の@smaguchiさん、発表の機会を与えていただきありがとうございました。また、勉強会の際に私がKVM on Dockerは出来るけどKVM on KVMはできないと話したんですが、後日@plansetさんから可能にする方法を教えていただきました。参考→KVM on KVM(nested KVM)。ありがとうございました!