/var/log/soymsk

id:soy_msk


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」

shop.oreilly.com

で、元々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におまかせしちゃって、 機能単位でコンテナクラスタにデプロイしてしまえば、いろんなコンポーネントの運用がかなり楽になりそうで、応用範囲めちゃ広いと思いました。

次は実際のシステム構築をやりつつ、もう少しベストプラクティス的な内容を見ていこうと思います。

cloudplatform.googleblog.com

どうでもいいですけど、Kubernetesってめっちゃタイポしますね。

pyenv + venv + direnv で作るPython3環境

pyenv + virtualenvの問題点

Pythonでの開発環境において、

  • 開発ホスト上に複数のバージョンのPythonをインストールしたい場合 … pyenv
  • プロジェクトごとに仮想環境を構築し、モジュールを切り替えたい場合 … virtualenv

の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にも載っています。

qiita.com

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
    • これまで運用している中で、度々「なぜかシャードアロケーションが進まない」といった出来事があり、よくよく調べてみるとdisk watermarkに引っかかっていたなど、わかりにくい状況が多かったので、その理由を詳細にレポートしてくれる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 からの差分として記載しています。ご指摘ありがとうございます。

f:id:soy_msk:20161211021259p:plain

  • 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がtextkeywordのマルチフィールドになっているので、何も考えずにデータを入れると、両方のフィールドができ、検索も可能になります。

それはいいのですが、容量もその分増える ことを考えると、データ量の多いフィールドなどはやはり事前にマッピング定義しておいたほうが無難かと思います。


インデックス内で同じ名前を持つフィールドは同じ型にしなければならない制約

これは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機能を使えば直ぐにできました。 f:id:soy_msk:20161211015027p:plain

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 にドット付きフィールドを入れてみました。 f:id:soy_msk:20161211015015p:plain

ただし、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)にアクセスできないことがわかった。

現象としては、

  • ping は通るが、80ポートの返答がない
  • https, httpなどプロトコルにはよらない
  • VPNサーバーとして動かしているLinux(CentOS)上からはアクセスできる。

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してしまうわけだが、まさに発生している現象とそっくりだ!

実際、VPNサーバー上でtcpdumpを取ってみると、

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, OS Xのネイティブクライアントで動作できていない。 OSX側の問題の可能性もあるので、解決したら記事更新予定。

VPN方式の違いについては、前回記事でまとめている。 soymsk.hatenablog.com

乱数精度の確認

参考:

証明書発行で利用する乱数の精度を確認する。

ホスト上のエントロピーは下記のコマンドで確認できる。 以下の値が1000以上なら問題ないらしい。

# cat /proc/sys/kernel/random/entropy_avail
66

足りてない。。

# yum install rng-tools
#  cat /dev/random | rngtest  -c 1000

乱数生成が遅いのか、上記コマンドも全く返ってこない。

乱数用のエントロピーが低すぎる状態であることはわかったので、havegedをインストールし、エントロピーの生成を増やしてみる。 (下記はVPNの構築については必須ではない)

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

続いて、作成したルート証明書秘密鍵から、公開鍵を生成する。

  • CNと--sanにはドメイン名あるいはIPアドレスを指定できるが、これはVPNサーバーのものである必要があるので注意
  • --flag に以下の設定を入れている。
    • Windowsのクライアントからアクセスする場合には、--flag serverAuthが必要。
    • OS X 10.7.3以前のOSXでは、--flag ikeIntermediateが必要。
# 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: もう一度パスワードを入力

作りたいクライアント数分、上記のファイルを作成する。 クライアントには、

を登録して、パスフレーズを入力すれば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にて。

soymsk.hatenablog.com