BigQuery ML でボストン住宅価格予測
Cloud Nextで発表されたBigQuery MLを使って、ボストン住宅価格予測をやってみます。
BigQuery ML
Cloud Next’18 のKeynoteで発表された、BigQueryのSQLクエリ内で機械学習モデルの学習/推論ができる機能です。 ドキュメントはこちら
特徴としては、
- BigQuery内で機械学習モデルの学習&推論ができる機能
- 学習、推論コードはすべてSQL
- 学習、推論用のデータはBigQuery内のデータで行う。
- model_typeとして指定できるのは、今の所下記の2つ
- linear_reg
- logistic_reg
- 学習モデルはBQ内に永続化され、モデル自体のダウンロードは(今の所)できない
モデルが2種類しかないのですが、とりあえず試す分には問題ないでしょう。 今後追加実装もされるようです。
データセットの準備
scikit-learnに含まれる、Boston house-pricesのデータセットを使います。 データセットの詳細はこちらを参考にしています。
今回、下記のPythonパッケージを使います
- scikit-learn
- pandas
- pands-gbq
import pandas as pd from sklearn.datasets import load_boston from sklearn.model_selection import train_test_split boston = load_boston() X = pd.DataFrame(boston.data, columns=boston.feature_names) display(X) y = pd.DataFrame(boston.target, columns=['price']) display(y)
scikit-learnで予測
比較のために、scikit-learnで予測してみます。
from sklearn.linear_model import LinearRegression (X_train, X_test, y_train, y_test) = train_test_split(X, y, test_size=0.1, random_state=0) model = LinearRegression() model.fit(X_train, y_train) predicted = model.predict(X_train)[:, 0] result = y_train.copy() result['predicted'] = predicted result.sort_index().plot()
import sklearn result = y_test.copy() predicted = model.predict(X_test)[:, 0] result['predicted'] = predicted result.sort_index().plot() "score: %s" % (sklearn.metrics.regression.r2_score(result['price'], result['predicted']))
'score: 0.5151316073718609'
今回は精度とか気にしないのでこれでいいでしょう。
BigQueryへのデータセット投入
先程のデータセットをBQに投入します。特徴量と正解値を同じテーブルに入れたほうがラクなので、 DataFrame上で結合してから、BigQuery APIで入れちゃいます。
また、あらかじめtest_dataset
という名前でBigQuery データセットを作成してあります。
PROJECT_ID = XXXX # GCPプロジェクトID train_data = X_train.copy() train_data['price'] = y_train train_data test_data = X_test.copy() test_data['price'] = y_test test_data train_data.to_gbq("test_dataset.boston_train", PROJECT_ID) test_data.to_gbq("test_dataset.boston_test", PROJECT_ID)
BigQuery ML
学習
データの準備ができたので、BigQuery MLで、さっそくモデルを学習させてみます。
以下のSQLを打てば、BQで学習が始まります。
CREATE OR REPLACE MODEL `test_dataset.boston_predict_model` OPTIONS (model_type='linear_reg', input_label_cols=['price']) AS SELECT * FROM `test_dataset.boston_train`
CREATE OR REPLACE MODEL
でモデルを作成します。モデル名は [dataset].[model name]
と指定します。
OPTIONS
ではモデルの設定が可能です。ここではモデルとしてlinear_reg
、また正解データのラベルは作成したデータ・セットのprice
を指定しています。
学習用のデータはSELECT XXX
で指定します。テーブルには特徴量と正解ラベルが一緒に入っているので、BQが区別できるよう、input_label_colsで正解データの列を指定しているわけです。
学習が成功すると、モデルが作成されます。
BQのUIから見ると、データセット内に、テーブルと同じように格納されていることがわかります。
評価
ML.TRAINING_INFO
で学習の収束状況などを確認できます。
SELECT * FROM ML.TRAINING_INFO(MODEL `test_dataset.boston_predict_model`)
また、ML.EVALUATEでモデルの評価もできます。
SELECT * FROM ML.EVALUATE(MODEL `soymsk-gcp.test_dataset.boston_predict_model`, ( SELECT * FROM `test_dataset.boston_test`))
結構lossが大きいようですが、iteration回数は調整できるので、今回はこのままいきます。
ちなみに、データを入れる際にラベルを指定していませんが、ラベルはModel側が記憶しているので、特に指定する必要はありません。逆に、学習時と同じラベル(カラム名)でデータを入れる必要があります。
推論
学習済みのモデルを使った推論もSQLクエリで行います。
SELECT price, predicted_price FROM ML.PREDICT(MODEL `soymsk-gcp.test_dataset.boston_predict_model`, ( SELECT * FROM `test_dataset.boston_test`))
ML.PREDICTの第一引数に学習済みモデルをMODEL [model name]
の形式で指定し、第2引数に特徴量を入れます。ここでは先程作成したboston_testテーブルのデータを入れています。
カラム名も同じなため、改めて指定し直す必要もありません。
精度はチューニングしていないのでそれほど良くはありませんが、SQLクエリだけでMLモデルを作って推論することができました。
BigQuery MLの価格
ここが気になるところですが、ドキュメントには
Currently, if you use BigQuery on demand, your BigQuery ML charges are based on the data processed by each query. For BigQuery ML queries, the data processed is usually greater than the just the input data for the CREATE MODEL statement. Flat-rate customers can use their existing slots for BigQuery ML until Jul 31, 2019.
とあるので、他のSQLクエリ同様、処理データ量に依存するみたいです。
が、書いてあるとおり、処理データ > 全データ量
となる場合もあるらしく、反復計算するので当たり前といえばそうですが、ここは気にしておいたほうが良さそうです。
なんとなく 「全データ量 x itreration数」が上限だと予想しますが、そもそも価格ロジック自体がまだβであり、将来的に変更となる可能性があります。
まとめ
今回BQで簡単な線形回帰をやってみました。 簡単なモデルしか用意されていないですが、BigQuery上に大規模なデータセットを持っている場合、いちいちSparkML等でやるのも骨が折れますが、BigQueryのマシンリソースを使って誰でも簡単に機械学習が試せる、というのはすごいことだと思います。
個人的には時系列予測や異常検知系のニーズが高いので、そこが実装されると異常検知しつつStackdriver経由でアラートあげるとか、普通にやるといろいろなコンポーネントを組み合わせないと実現できない構成がぐっとラクに作れるようになるのではと期待しています。
入門Kubernetes 読んだ
最近、Kubernetes(k8s)盛り上がってますね。
KubernetesといえばMicroservice、というイメージでしたが、 意外と機械学習系のプロダクト開発と相性がいいのではないかと思うようになり、 しっかり勉強することにしました。
Kubernetes + 機械学習といえば、メルカリさんは MLOps として、すでにプロダクトに組み込んでいるそうですね。
学習データやパラメータに依存性がある機械学習システムって単に学習・推論コードをVersioningするだけでは不十分なので、まるっと コンテナで管理できる世界にできたらいいな、なんて妄想しています。
Oreilly「入門 Kubernetes」について
原書は「Kubernetes: Up and Running」
で、元々GoogleのKubernetesチームで働いていた方によるものです。 原書の発行が2017/09で、日本語版が2018/03なので、非常に早く翻訳版が出たように思います。感謝。
内容については、全般に渡りチュートリアルの形式をとっており、自分でKubernetesクラスタを作りながら学んでいく形式です。 ちなみにコードやコンテナイメージは https://github.com/kubernetes-up-and-running に公開されています。
構成
本書は以下のような構成になっています。
- 1章: Kubernetes入門
- なぜKubernetesを使うのか、利点など
- 2章: コンテナの作成と起動
- Dockerの説明。Dockerを知っている人なら読み飛ばせます
- 3章: Kubernetesクラスタのデプロイ
- 4章: よく使う kubectlコマンド
- ほぼすべての操作はkubectl経由で行うのでしっかり読んでおくとよいです。
- 5章~13章:
- 一通りKubernetesの機能の説明
- 14章: 実用的なアプリケーションのデプロイ
- さっと読んだだけ。実際のアプリケーションを例に設計の考え方が書かれています。
一通り読んでみて、Kubernetesの大まかな機能を把握できました。
チュートリアル形式なので、1からやっていけばKubernetesの何が良くて、どのように構成されているのか理解できます。
Kubernetes公式ドキュメントと併読するのがおすすめ
難点というか、チュートリアル形式であるため、どうしても個々の機能/用語の解説が簡素というか分かりづらい部分はありますね。 たとえば3章の段階でひとまずクラスタを作るのですが、ちょくちょくまだ説明されていない単語が出てきて困惑します。
また用語の説明もたまに分かりづらいものがあり、例えば4章の冒頭でNamespaceの説明があるのですが、
Kubernetes は、クラスタ内のオブジェクトを構造化するために、Namespaceを 使います。各 Namespace は、オブジェクトの集まりを入れるフォルダだと考えれば よいでしょう。
うーん、初学者には何がなんだかわかりませんでした。。
公式ドキュメントを見ると、
Kubernetes supports multiple virtual clusters backed by the same physical cluster. These virtual clusters are called namespaces.
(https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ ) とあり、1つの物理クラスタを仮想クラスタで分割するための仕組みであることがスッと理解できました。
その他でいうとVolumeとPersistentVolume(PV)が別々の章で説明されており、それぞれがお互いどう関係しているのか?PVはVolumeの一種なのか? よくわからず、公式ドキュメントで補完しました。
とはいえ、公式ドキュメントは最初から読むには体系化されすぎていて逆につらいと思うので、この本を読みつつ、詰まったら公式ドキュメントを参照する、というやり方が近道だと思います。
あと面白いのが、Raspberry Pi上でKubernetesクラスタを構築する方法が付録で解説されているところですね。 なぜRaspberry Pi上なんでしょう(笑) とはいえ、クラウドを使わなくとも低スペックのマシンでクラスタオペレーションが試せるのは自習としてとてもよいですね。
読んでみて、今後
機械学習系以外でも、最近βで発表されたGoogle Cloud ComposerなんかはマネージドAirflowなわけですが、 バックエンドはKubernetes(GKE)で動いているようです。
オートスケールや耐障害性なんかの特性はすべてKubernetesにおまかせしちゃって、 機能単位でコンテナクラスタにデプロイしてしまえば、いろんなコンポーネントの運用がかなり楽になりそうで、応用範囲めちゃ広いと思いました。
次は実際のシステム構築をやりつつ、もう少しベストプラクティス的な内容を見ていこうと思います。
どうでもいいですけど、Kubernetesってめっちゃタイポしますね。
Rundeck で履歴を定期パージするためのツール
Rundeck はジョブヒストリーを定期自動パージできないという問題があり、 いつの間にか数万エントリーになっていたりするので、作りました、という告知
pyenv + venv + direnv で作るPython3環境
pyenv + virtualenvの問題点
Pythonでの開発環境において、
の2つの組み合わせは一般的に使われているものかと思います。
virtualenvにおける、仮想環境切り替えの煩わしさ
virtualenvで開発環境を構築した場合、仮想環境 を切り替えるためには、プロジェクトのディレクトリ配下で、毎回 source ./{virtualenv directory}/bin/activate
しなければなりません。
この、仮想環境の切り替えを手動で行うのは開発中なかなか面倒なのと、いろいろなプロジェクトを触っているとついつい忘れがちになります。
pyenv + pyenv-virtualenvにするか?
pyenv と pyenv-virtualenv(プラグイン) を使えば、プロジェクトディレクトリにcdしたときに仮想環境を自動で切り替えてくれるので、手動による煩わしさはなくなります。
しかしながら、pyenvが管理するバージョンの中に、Python自体のバージョンと、virtualenvによる仮想環境が混在する状態になってしまいます。 プロジェクトが少ない段階ではこれでも十分なのですが、プロジェクトが増えてきた場合に、非常に管理しづらくなってきます。
このように、基本的な仮想環境の管理はvirtualenv方式がよいのですが、その仮想環境の切り替えは自動でやりたい、という場合には、direnvを使うとよさそうです。
そこで今回、 pyenv + venv(virtualenv) + direnv を使って、Python3開発環境を構築してみました。
pyenv のインストール
今回、venvを使うのでPython3 を入れておきます。
pyenv install 3.6.1 # versionは3.xのものを適宜 # 適当なテスト用プロジェクトディレクトリを作成 mkdir ~/myproject cd ~/myproject pyenv local 3.6.1 pyvenv my_venv
direnv
direnvはディレクトリごとに環境変数を切り替えることができるツールで、特定のディレクトリにいるときだけ環境変数をsetし、ディレクトリから抜けるときにunsetということをすべて自動的に行ってくれます。
Qiitaにも載っています。
direnv のインストール
インストールは非常に簡単です。
git clone http://github.com/zimbatm/direnv cd direnv sudo make install
また、以下の内容を.zshrc
に追加します。
eval "$(direnv hook zsh)" show_virtual_env() { if [ -n "$VIRTUAL_ENV" ]; then echo "($(basename $VIRTUAL_ENV))" fi } PS1='$(show_virtual_env)'$PS1
2行目以下は、direnvを使った場合にそのままでは仮想環境名がプロンプトに表示されないのですが、 その対策のために必要になります。
direnvの使い方
環境変数を切り替えたいディレクトリ、ここでは開発プロジェクトのディレクトリ配下に.envrc
というファイルを作成します。
direnvは、cd先のディレクトリ配下にこの.envrc
スクリプトが存在する場合にそのスクリプトをbashで実行し、スクリプト内でexportされた環境変数を、現在のシェルにsetします。
$ cat ./.envrc HOGE="fuga" export HOGE # この変数がディレクトリ以下でのみ有効になる
また、このディレクトリから更に別のディレクトリに移動した場合には、setされた環境変数は自動でunsetされます。
この機能を利用して、venvの開発環境の自動切り替えを実現させます。
.envrc
.envrcには以下のようにして保存します
source ./my_venv/bin/activate
これだけで準備完了です。
仮想環境の自動切り替え
以上の準備ができたらシェルを再起動して試してみます。
$ cd ~/myproject direnv: loading .envrc direnv: export +VIRTUAL_ENV ~PATH (my_venv) $ $ which pip ~/myproject/my_venv/bin/pip
自動で仮想環境が有効になりました。
※direnv: error .envrc is blocked. Run 'direnv allow' to approve its content.
というエラーが出た場合は direnv allow .
を実行します。
また、ディレクトリから抜けると、仮想環境が自動的に無効になることもわかります。
$ cd ~/ direnv: unloading $ which pip ~/.pyenv/shims/pip
仮想環境の自動切り替えが実現できました! これでプロジェクトが増えても安心できそうです。
また、今回pyvenvを使いましたが、virtualenvでも同様にできるはずですので、Python2環境にも応用できるかと思います。
Elasticsearch 5.xの新機能と、1.xから5.xへのアップグレード
この記事は、Elastic stack Advent Calendar 2016の11日目の記事です。
Elasticsearch クラスタを1.7 から 5.0.2まで上げたときに、それなりに変更点が多く、苦労しました。
本記事では、そのときのノウハウを元に、
- 1.xを使っていて、5.xに上げたいと思っている
人を主な対象に、アップグレードの注意点や、1.xからの追加・変更機能について書きたいと思います。
世の中的にすでに2.xの人が多いと思いますが、数ヶ月前にElasticsearch勉強会に参加した際に、まだ1.xを利用している人がそれなりにいたので、今でもまだ役に立つかなと思います。
とはいえ ver2.xからの変更点も含むので、今2.xを使っている人にも参考になるように書きます。
モチベーション
- Reindex APIが使いたかった。
- Kibanaでダッシュボードの共有URL(shorten url)が使いたかった
いい加減、上げたかった技術的な興味
5.xで追加・変更される機能について
Elasticsearch
公式より個人的にうれしかったものから
- バリデーション強化
IngestNodeなどが注目されるところですが、個人的にはまずいろいろなAPIのバリデーションが強化されており、何か間違っていてもすぐに分かるようになったというのが大きいです。
- Reindex API
- 2.xからありますが、別クラスタからも取り込めるようになったみたいです。アップグレード時に大活躍してくれます(後述)
- フィールド名にドットがまた使えるようになった。(後述)
- Ingest Node
- 目玉機能かと思いますが、まだプロダクトで使ってません。すでに他の記事があるので割愛
- Indexing パフォーマンス改良
- ドキュメントの類似度算出アルゴリズムがTFIDFからBM25に変更
- Cluster Allocation Explain API
- 独自のスクリプト言語Painlessの導入
- search_after
- ページング処理が楽にかけるようになりそう。そのうち別に記事を書きたいです。
Kibana
- shorten URL
- モチベーションの1つ
- https://github.com/elastic/kibana/issues/1553
- ログ監視していて、通知にこれをつけておくと超捗る
- 公開短縮URLサービスを使えない場合も多いはず。
- (追記 2016/12/12)
- こちらの機能はすでにKibana4.4から導入されているので、Kibana5の機能ではありません。ES 2.2以上を使っていればKibana4.4も使えるので問題ないですね。 今回ES1.x からの差分として記載しています。ご指摘ありがとうございます。
- Sense ( => Console ) がデフォルトで入ってる。
この他にも_cat/**
のAPIが追加になっていたり、運用で触っていると、ところどころ使いやすくなっているなと感じます。起動時にカーネル設定もみてくれて、推奨値でない場合は警告を出してくれます。
ということで、機能というより、運用のしやすさが上がったと個人的には感じています。
残念だったところ
総じてよかったのですが、残念なところもあります。
- head, HQと言ったWebUIを持つプラグインがことごとく使えない。
- Elasticsearch側の仕様変更によるものみたいですが、ヘビーユースしていただけに辛いです。Webサーバーとして別個に構築する方法や、そもそもkibanaにx-packいれてmarvelに乗り換えたりと、方法はいろいろありますが、コマンド一発でインストール&どのノードでもアクセス可能な魅力にはかなわず。
- headに関して、Kibana pluginとして開発し直す話もあるようですが。。
5.xへのアップグレード
続いて、今回1.xからアップグレードする過程で注意する点やtipsをまとめます。
メジャーバージョン間でのアップグレードなので、ローリングアップグレードはできません。
JVM
java8必須になっています
また、JVMオプションの設定については
これまで bin/elasticsearch.in.sh
などに記述していましたが、conf/jvm.options
という別ファイル切り出して記述することが推奨されています。
-Xms2g -Xmx2g
設定に関する変更
index.*** の設定
これまで config/elasticsearch.yml
に書いていたindex.xxx
の項目がかけなくなりました。
代わりにindex mappingに全てまとめられたようです。 例えば、今後作成するindexのデフォルト設定を変更したい場合は、index templateを利用すればよいかとおもいます。
PUT /_template/default_template { "order":0, "template": "*", "settings": { "index": { "number_of_shards": "4", "number_of_replicas" : "2" } } }
Index Mappingに関する変更
1.xからだとかなり変更があります
String 型の廃止
これまで文字列データに対してString型があり、analyzed, not_analyzedオプションで検索可能フィールドかどうか設定していましたが、5.xからはtext
, keyword
という新しい2つの文字列型が導入され、明確に区別しています。
文字列型のdynamic mappingがtext
とkeyword
のマルチフィールドになっているので、何も考えずにデータを入れると、両方のフィールドができ、検索も可能になります。
それはいいのですが、容量もその分増える ことを考えると、データ量の多いフィールドなどはやはり事前にマッピング定義しておいたほうが無難かと思います。
インデックス内で同じ名前を持つフィールドは同じ型にしなければならない制約
これはtypeをまたいだフィールドにも適用されるので、
- index1.type1.name
- index1.type2.name
というフィールドは同じ型にしなければならないという制約が入っています。
'ドット'をフィールド名に持つフィールドの扱い
これは紆余曲折がありまして、2.xでドットフィールドが使えなくなりましたが、5.xでサポートが復活しています。
ただし まったく同じというわけではなく、ドットでつながったフィールドは内部的にハッシュオブジェクトとして管理されているため、例えば
A.B.C A: object type B: object type C: *
となりますので、Aを別のフィールド名につかっているとNGです。
user: 'john' # keyword型としてまず登録 user.age: 10 # => userをobject型として使っているのでError
フィールド名に'ドット(.)' をが含まれるフィールドがある場合には注意が必要です。
クエリに関する変更
ver2.xで非推奨になったものも5.xでは廃止になっているので対応が必須です。
ファセットの廃止
これは2.xですでに廃止されているので今更ですが、1.xからの人には念のため。
1.7でもaggregationが使えるので、書き換えておきましょう。 aggregationの方が書きやすいですね。
filteredクエリの廃止
2.xでdeprecatedになっていたものが廃止になっています。 boolクエリに書き換えます
# ver1.x "query": { "filtered": { "query": {"should": []}, "filter": {} } } # ver5.x "query": { "bool": { "should": [], "filter": {} } }
他にも細々ありますが、問題になったのは上2つでした。
データ移行には Reindex APIを活用する
アップグレードに際し、自分は5.xのクラスタを新たに構築し、順次移行していく手順を取りました。
その際に困るのがデータ移行ですが、5.xになってリモートからの取り込み機能が追加されています。
これが超便利
POST _reindex { "source": { "remote": { "host": "http://old-cluster-node:9200" }, "index": "logstash-2016.10", "size": 1000 # chunk size }, "script": { "inline": "ctx._source.put('new_field', ctx._source.remove('old_field'))" }, "size": 100000 # total reindex docs "dest": { "index": "logstash-2016.10" } }
これだけでデータコピーができてしまいます。簡単ですね。
さらにデータ移行と同時に、
script
でフィールド名を書き換える- 1回あたりのコピーサイズを制限する
も行っています。
ただし、コピー元となるノードを予めコピー先クラスタの設定内に指定しておかないといけません。
# config/elasticsearch.yml reindex.remote.whitelist: old-cluster-node:9200
Kibanaのダッシュボード移行
これは元々Kibana4を使っていたので export/import機能を使えば直ぐにできました。
migration plugin
アップグレードする前にインストールしておくと、アップグレード後に非推奨、廃止になる機能を使っているインデックスに対してはWebUIで警告を出してくれ、どうすればよいかアドバイスしてくれます。
# install $ ./bin/plugin -i migration -u https://github.com/elastic/elasticsearch-migration/releases/download/v1.18/elasticsearch-migration-1.18.zip
http://localhost:9200/_plugin/migration/ にアクセスすると診断してくれます。
試しにindex1.type1 にドット付きフィールドを入れてみました。
ただし、1.x => 2.x , 2.x => 5.x とプラグインが分かれていて、直接1.x => 5.xのmigrationをみてくれるわけではないので注意。
実際Reindex APIでデータ投入しちゃったほうが確実でした。
まとめ
まだメジャーバージョンが出たばかりというところですが、 プロダクションで使えるかという点においては、少なくともバックエンド的な使い方では十分使えそうという印象でした。
ただこの記事執筆時点ですでに5.1.1までリリースされており、そのリリースサイクルを見ると、今後も頻繁にアップグレードしていく必要はありそうです。
CentOSでVPN(IKEv2) を 構築する with StrongSwan Part.2 ~ MTU値の調整 ~
特定のサイトにつながらない
前記事で設定したVPNサーバーを経由した場合に、特定の一部サイト(github.com, yahoo.com)にアクセスできないことがわかった。
現象としては、
tracerouteの結果は、最初の数段は通るが、以降はtimeoutしてしまっていた。
これもかなりハマってしまったが、原因を探してみると、どうやら経路間のMTU値の設定がVPNを経由した場合にうまくいかないようだ。
PMTUD
MTUはMaximum Transmission Unitの略で、1フレームごとに送信するデータの最大値を意味する。 MTUは現在ではデフォルト1500になっていることが多いが、経路によってはこの値が異なる場合がある。
自宅にはNTT東日本のFTTHを引いているが、PPPoE経路上では追加のヘッダーが付与される関係上この値が小さくなる。 実際に自宅のルーターの設定を覗いてみるとMTU= 1454バイトに設定されていた。
通信の際には、経路上で最小のMTU値に合わせてデータを送信する必要がある。 もし、最小MTU値を超えたフレームを送信した場合、フレームを分割送信するフラグメンテーションを行って再送する必要がある。(というICMPパケットが送信先から返ってくるらしい。あまり詳しくない。。)
で、このフラグメンテーションはパフォーマンス上大変よろしくないので、できれば最小MTU値で送信してフラグメンテーションしないようにしたい。 ところが、この最小MTU値は、接続先までの経路によって異なり、固定ではない。
そこで、その最小MTU値を決定するために、PMTUD(Path MTU Discovery)がある。 PMTUDの詳細についてはググれば出てくるので割愛。
ウェブサイトへのアクセスが失敗する理由
さて、問題はここから。
PMTUDはICMPパケットを利用するため、一部の経路でこれが遮断されていると使えない。 ICMPは本来遮断すべきではないのだが、pingを塞ごうとしてICMPまるごと塞いでしまっている経路が存在する。
その場合、フラグメンテーションによって解決されるわけだが、そこに今回のVPN特有の問題がある。
によると、
Instead of fragmenting a too-large IP packet, the VPN server is told (through the Don’t Fragment (DF) flag in the IP header of the sender) to discard the packet and reply with an ICMP fragmentation required (type3, subtype 4) message.
本来、アクセスしたいウェブサーバーが、このMTU値を超えた通信を行ってきた場合に、VPNサーバーはそのパケットを破棄し、代わりにICMP fragmentation required を返す。
When the sender receives this ICMP packet, it learns to use a smaller MTU for packets sent to our VPN server. In theory. In reality, many websites (senders like www.yahoo.com) stupidly implement ICMP filters that break PMTUD functionality.
本来であれば、このパケットを受け取ったサーバーはMTU値を調整して再送してくれるはず(google.comなど)だが、一部のサーバーではこれがうまく実装されておらず、延々とack待ちになってしまう。 この実装の違いが、VPNを通した場合にアクセスできたりできなかったりするサーバーの違いだ。
結果VPNクライアントからみると通信がtimeoutしてしまうわけだが、まさに発生している現象とそっくりだ!
21:38:09.594323 IP {VPNサーバー} > {接続先IP}: ICMP {VPNサーバー} unreachable - need to frag (mtu 1374), length 556
というようなログが出ている。やはりMTU値が問題なようだ。
CentOS7 firewalldでのMTU値の調整
対応として、参考サイトにはiptablesの設定がのっている。
$ iptables -t mangle -A FORWARD -o eth0 \ -p tcp -m tcp --tcp-flags SYN,RST SYN \ -s 192.168.12.0/24 \ -m tcpmss --mss 1361:1536 \ -j TCPMSS --set-mss 1360 $ echo 1 >/proc/sys/net/ipv4/ip_no_pmtu_disc
1行目
-s 192.168.12.0/24
はクライアントのソースIPで条件指定している。ここでは前回設定したVPNクライアントのサブネットを指定--mss 1361:1536
でMSSが1361 ~ 1536の間にあるパケットだけ絞って設定を適用できる。
2行目
- UDPのための設定
今回はCentOS7上での設定となるため、これを書き換えて以下のコマンドを実行した。
CentOS7, Firewalld
# firewall-cmd --direct --permanent \ --add-rule ipv4 mangle FORWARD 1 \ -p tcp -m tcp --tcp-flags SYN,RST SYN \ -s 192.168.12.0/24 \ -m tcpmss --mss 1301:1536 \ -j TCPMSS --set-mss 1300 # echo 1 >/proc/sys/net/ipv4/ip_no_pmtu_disc # systemctl restart firewalld
MSSが1360では自分の環境では動作しなかったので、1300にしている
再起動が終わった後に試してみると、
ちゃんとVPNクライアントからgithub.com, yahoo.comにアクセスできるようになった!
長かった!
ちなみに
こちらのQiitaなどで紹介されている
firewall-cmd --direct --add-passthrough ipv4 -I FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
は上手くいかなかった。どうやらVPNクライアントでは上手く動作しないようだ。
CentOSでVPN(IKEv2) を 構築する with StrongSwan Part.1
基本的な手順は参考元サイトをもとに構築していく。
- 想定クライアント
- iOS: > 10
- OS X: > El Capitan
※今回、最終的にユーザー名・パスワード認証は成功しているが、公開鍵認証はiOS, OS Xのネイティブクライアントで動作できていない。 OSX側の問題の可能性もあるので、解決したら記事更新予定。
VPN方式の違いについては、前回記事でまとめている。 soymsk.hatenablog.com
乱数精度の確認
参考:
- https://wiki.archlinuxjp.org/index.php/Haveged
- http://abyssluke.hatenablog.com/entry/2015/09/24/205723
証明書発行で利用する乱数の精度を確認する。
ホスト上のエントロピーは下記のコマンドで確認できる。 以下の値が1000以上なら問題ないらしい。
# cat /proc/sys/kernel/random/entropy_avail 66
足りてない。。
# yum install rng-tools # cat /dev/random | rngtest -c 1000
乱数生成が遅いのか、上記コマンドも全く返ってこない。
乱数用のエントロピーが低すぎる状態であることはわかったので、havegedをインストールし、エントロピーの生成を増やしてみる。 (下記はVPNの構築については必須ではない)
haveged
- HAVEGEアルゴリズムで乱数生成のエントロピーを生成するツール
- Linuxの
/dev/random
はデバイスからの環境ノイズを利用したエントロピーを利用しており、エントロピーの質は高いが、その分エントロピー生成が遅い。 - havegedを利用することで、擬似乱数にはなってしまうが、エントロピー生成を加速させることができる。
havagedのセットアップ
$ sudo yum install haveged $ sudo systemctl enable haveged $ sudo systemctl start haveged $ sudo cat /proc/sys/kernel/random/entropy_avail 2273
これで、エントロピーが1000を超えて生成されるようになった。
Install Strongswan
StrongswanをインストールはyumでOK
$ sudo yum install strongswan
認証鍵の作成
つづいて、IKEv2で使うサーバー側のCA認証鍵の生成を行う。 認証鍵は、strongswanのpkiサブコマンドで作成できる。
ルート証明書の作成
自己署名のルート証明書(いわゆるオレオレ証明書)を作成していく。
自己署名のルート証明書について
今回は自己署名のルート証明書を作成したが、 VPN接続の前に、クライアントにルート証明書をインストールする必要がある。
公的なCAから証明書発行が得られるのであれば、クライアントにルート証明書をインストールする手間が不要になるのでそちらがオススメ。
まずはルートサーバーの秘密鍵を作成する。
# cd /etc/strongswan # strongswan pki --gen --type rsa --size 4096 --outform der > ipsec.d/private/strongswanKey.der # chmod 600 ipsec.d/private/strongswanKey.der
続いて、ルート証明書を作成。(dnの中身は適宜書き換えること)
# strongswan pki --self --ca --lifetime 3650 --in ipsec.d/private/strongswanKey.der --type rsa --dn "C=JP, O=soymsk, CN=strongSwan Root CA" --outform der > ipsec.d/cacerts/strongswanCert.der # 作成したルート証明書の内容確認 # strongswan pki --print --in ipsec.d/cacerts/strongswanCert.der subject: "C=JP, O=soymsk, CN=strongSwan Root CA" issuer: "C=JP, O=soymsk, CN=strongSwan Root CA" validity: not before Oct 07 22:41:11 2016, ok .....
VPNサーバーの認証鍵の作成
VPNサーバー自身の秘密鍵を作成
# strongswan pki --gen --type rsa --size 2048 --outform der > ipsec.d/private/vpnHostKey.der # chmod 600 ipsec.d/private/vpnHostKey.der
続いて、作成したルート証明書と秘密鍵から、公開鍵を生成する。
# strongswan pki --pub --in ipsec.d/private/vpnHostKey.der --type rsa | strongswan pki --issue --lifetime 730 --cacert ipsec.d/cacerts/strongswanCert.der --cakey ipsec.d/private/strongswanKey.der --dn "C=JP, O=soymsk, CN=soymsk.example.jp" --san soymsk.example.jp --flag serverAuth --flag ikeIntermediate --outform der > ipsec.d/certs/vpnHostCert.der # strongswan --issue --help # --cacert: CA certificate file. 作成したルート証明書のパス # --cakey: CA private key file. 作成したルート秘密鍵のパス
作成したサーバー公開鍵は、以下のコマンドで詳細を確認できる。
# strongswan pki --print --in ipsec.d/certs/vpnHostCert.der subject: "C=JP, O=soymsk, CN=soymsk.example.jp" issuer: "C=JP, O=soymsk, CN=strongSwan Root CA" validity: not before Oct 08 08:06:09 2016, ok not after Oct 08 08:06:09 2018, ok (expires in 729 days) ...... # OpenSSLを使っても確認できる # openssl x509 -inform DER -in ipsec.d/certs/vpnHostCert.der -noout -text
クライアントの認証鍵を作成
ルート証明書を使って、今度はクライアントの認証鍵を発行する。 やり方はサーバー認証鍵のものと同じような流れになるが、CNの部分がユーザー名@ドメイン
の形になる。
秘密鍵の作成
# cd /etc/strongswan/ # strongswan pki --gen --type rsa --size 2048 --outform der > ipsec.d/private/vpnClientKey.der # chmod 600 ipsec.d/private/vpnClientKey.der
公開鍵の作成
# strongswan pki --pub --in ipsec.d/private/vpnClientKey.der --type rsa | \ strongswan pki --issue --lifetime 730 --cacert ipsec.d/cacerts/strongswanCert.der --cakey ipsec.d/private/strongswanKey.der \ --dn "C=JP, O=soymsk, CN=vpnuser@soymsk.example.jp" --san vpnuser@soymsk.example.jp" \ --outform der > ipsec.d/certs/vpnClientCert.der
クライント秘密鍵、公開鍵、ルート証明をPEM形式に変換したのち、パスワードつきでPKCS#12 ファイルにまとめる。
# openssl rsa -inform DER -in ipsec.d/private/vpnClientKey.der -out ipsec.d/private/vpnClientKey.pem -outform PEM writing RSA key # openssl x509 -inform DER -in ipsec.d/certs/vpnClientCert.der -out ipsec.d/certs/vpnClientCert.pem -outform PEM # openssl x509 -inform DER -in ipsec.d/cacerts/strongswanCert.der -out ipsec.d/cacerts/strongswanCert.pem -outform PEM # openssl pkcs12 -export -inkey ipsec.d/private/vpnClientKey.pem -in ipsec.d/certs/vpnClientCert.pem \ -name "my private VPN Certificate" -certfile ipsec.d/cacerts/strongswanCert.pem \ -caname "strongSwan Root CA" -out myVpnClient.p12 Enter Export Password: passwordを入力 Verifying - Enter Export Password: もう一度パスワードを入力
作りたいクライアント数分、上記のファイルを作成する。 クライアントには、
- ルート証明書
- .p12ファイル
を登録して、パスフレーズを入力すればOK。
認証鍵の生成はこれで完了。
IPsecの設定
続いて、IPsecの設定を行う。あと少し!
/etc/strongswan/ipsec.conf を次のように書く。
# ipsec.conf - strongSwan IPsec configuration file # basic configuration config setup charondebug="ike 2, knl 2, cfg 2, net 2, esp 2, dmn 2, mgr 2" # Add connections here. conn %default keyexchange=ikev2 ike=aes128-sha256-ecp256,aes256-sha384-ecp384,aes128-sha256-modp2048,aes128-sha1-modp2048,aes256-sha384-modp4096,aes256-sha256-modp4096,aes256-sha1-modp4096,aes128-sha256-modp1536,aes128-sha1-modp1536,aes256-sha384-modp2048,aes256-sha256-modp2048,aes256-sha1-modp2048,aes128-sha256-modp1024,aes128-sha1-modp1024,aes256-sha384-modp1536,aes256-sha256-modp1536,aes256-sha1-modp1536,aes256-sha384-modp1024,aes256-sha256-modp1024,aes256-sha1-modp1024! esp=aes128gcm16-ecp256,aes256gcm16-ecp384,aes128-sha256-ecp256,aes256-sha384-ecp384,aes128-sha256-modp2048,aes128-sha1-modp2048,aes256-sha384-modp4096,aes256-sha256-modp4096,aes256-sha1-modp4096,aes128-sha256-modp1536,aes128-sha1-modp1536,aes256-sha384-modp2048,aes256-sha256-modp2048,aes256-sha1-modp2048,aes128-sha256-modp1024,aes128-sha1-modp1024,aes256-sha384-modp1536,aes256-sha256-modp1536,aes256-sha1-modp1536,aes256-sha384-modp1024,aes256-sha256-modp1024,aes256-sha1-modp1024,aes128gcm16,aes256gcm16,aes128-sha256,aes128-sha1,aes256-sha384,aes256-sha256,aes256-sha1! dpdaction=clear dpddelay=300s rekey=no left=%any leftsubnet=0.0.0.0/0 # VPN Server証明書 leftcert=vpnHostCert.der leftsendcert=always right=%any rightdns=8.8.8.8,8.8.4.4 # rightsourceip: VPNクライアントが接続してきた際に、接続先ネットワークで割り振られるIPを指定する # この設定では、VPNクライアントには192.168.1.1などが割り振られることになる。 # VPNサーバーが所属するネットワークとは別に設定すること。 rightsourceip=192.168.1.0/24 conn IPSec-IKEv2 keyexchange=ikev2 auto=add # OSX, iOSクライアントは主にこちら conn IPSec-IKEv2-EAP also="IPSec-IKEv2" rightauth=eap-mschapv2 eap_identity=%any # OSX, iOSではleftid(VPN Server CN) leftid=@soymsk.example.jp
iOS8対応などは不要のため、元記事からいくつか設定を省いている。
VPNクライアント設定
クライアントの情報を設定するには、以下のファイルを編集する。
/etc/strongswan/ipsec.secrets
:
# 公開鍵認証 : RSA vpnHostKey.der # ユーザー名・パスワード認証 # `[<domain>\]<ユーザー名> : EAP "<パスワード>" ` vpnuser : EAP "EAPパスワード"
(パスワードがハッシュ化後のものでなく、平文なのがちょっと怖い)
ファイヤウォール、ルーター設定
インターネット側からNAT(ルーター)越しにアクセスを受け付けるために、FWとルーターの設定をする。
Firewalld 設定
# firewall-cmd --add-masquerade --permanent # firewall-cmd --query-masquerade --permanent # firewall-cmd --add-service="ipsec" --permanent # systemctl restart firewalld # firewall-cmd --list-all # 設定を確認
ルーター設定
インターネット側からのUDPの500, 4500ポートアクセスをVPNサーバーにポートフォワードするように設定する。
sysctlの設定
VPNサーバーからローカルネットワーク内のホストへIPフォワードを有効にするために、sysctlの設定を変更する.
(XX-の部分は優先度を決めるので、適切な数字で!)
/etc/sysctl.d/XX-vpn.conf
:
net.ipv4.ip_forward = 1 net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.all.send_redirects = 0
設定を反映
# sysctl -p
VPN サーバー起動
ようやくVPNサーバーの構築が終ったので起動してみる。
# systemctl enable strongswan # systemctl start strongswan # systemctl status strongswan strongswan.service - strongSwan IPsec IKEv1/IKEv2 daemon using ipsec.conf Loaded: loaded (/usr/lib/systemd/system/strongswan.service; enabled; vendor preset: disabled) Active: active (running) since Sat 2016-10-08 10:11:43 JST; 15s ago
どうやら起動に成功しているようだ。
クライアントからの接続設定
ちょっと長くなったので別記事にまとめる予定。
追記(2016/11/28)
これだけの設定では特定サイトにアクセスできない。 具体的にはMTU値を調整する必要があるが、詳しくはPart2にて。