st2clientを使ってリモート(st2-docker)のst2を操作する

# やりたいこと・目的

- [st2-docker](https://github.com/StackStorm/st2-docker)使うとサクッと環境を構築出来る
- でも `st2` コマンドや `st2ctl` を叩くのにいちいち `docker[-compose] exec` してコンテナに入るのめんどくさい
    - `packs.dev` ディレクトリを使って開発してる時に `st2 run` とか `st2 run packs.load` とかしょっちゅう叩く必要がある
- ホスト側に `st2` コマンド **だけ** 入れてリモート(ここではコンテナ)のStackStormを操作出来るようにすれば解決
    - `st2` コマンドはst2auth/st2api/st2streamに対してhttpでAPIの操作をしているだけなのでリモートのStackStormも実は操作出来るように実装されている
        - `--debug` オプションを付けて実行すると実際にどのエンドポイントを叩いてるのか見れる
    - 今回は公式Dockerイメージ(st2-docker)のStackStormを操作する対象とするが、基本的にコンテナでない環境でも同じように動かせる、はず
- ホストにログインする全ユーザーが、特に何か手動で追加の設定をしたり、意識をしたりする必要なく `st2` コマンドを叩いてDockerコンテナ内のStackStormを操作出来るようにすることをゴールとする

# セットアップ手順

## 環境

- ホストOS: CentOS 7
- StackStorm: ホストのDocker上に起動したコンテナ。公式の `docker-compose.yml` 使用。バージョンは2.5.0

DockerはCEの最新版、ホストで使うpython/virtualenv/pipはCentOS 7標準のレポジトリから `yum install` で入るものを使っている。

## ホストへのst2clientのインストール

StackStorm公式のパッケージ(yum/deb)を使ってインストールしようとすると `st2` パッケージをインストールすることになるのだが、こいつにはStackStormの全コンポーネントが入っているのでデカすぎる。代わりにpipで `st2client` をインストールする。

ここでは環境を分けるためにvirtualenvを作ることにする。ユーザーは、ホストにログインした後にこのvirtualenvをactivateすることで `st2` コマンドが使えるようになる。

```
virtualenv /usr/local/opt/virtualenvs/st2client
source /usr/local/opt/virtualenvs/st2client/bin/activate
pip install st2client

# CentOS 7のpipでは上記コマンドでは途中依存パッケージのインストールでコケる。
# 解決するためにpipを自体をアップグレードする必要がある。
pip install --upgrade pip
```

これで `st2` コマンドが使える…と思いきや、実は依存関係のパッケージが足りなくてコケる。エラーメッセージが出なくなるまで `pip install` を繰り返す。自分がやったときは最終的に以下のパッケージを追加でインストールする必要があった。

```
pip install argcomplete
pip install pytz
pip install jsonschema
pip install prompt-toolkit
pip install python-editor
pip install sseclient
```

この件はIssueもあがっている → [st2client missing python dependencies](https://github.com/StackStorm/st2/issues/3367)

続いて、接続先を指定するための設定を行う。 `st2` コマンドはデフォルトでは127.0.0.1のポート9100(auth),9101(api),9102(strem)に接続しにいくが、以下の3つの方法で接続先＝操作する対象のStackStormを指定することが出来る([ドキュメント参照](https://docs.stackstorm.com/reference/cli.html?highlight=cli%20reference))。

- コマンドラインオプションで渡す
- configファイルに書く
- 環境変数で渡す

コマンドラインオプションを毎回指定するのは面倒くさい、となるとconfigファイルを書くか環境変数で渡すことになるが、複数人がこのホストに接続して `st2` コマンドを使うことを想定する場合は環境変数を使うのがベスト。

それは何故かと言うと、configファイルは通常は `~/.st2` 配下にあるものを読み込むのだが、このホストにログインする全員が特に何か設定をすることなく特定のリモートにあるStackStormに接続に行くようにする、というのを実現するために共通のconfigファイルを用意して環境変数 `ST2_CONFIG_FILE` でそいつを読み込むようにしてしまうと `st2 auth login` がうまく使えなくなってしまう。というのも `st2 auth login` はconfigファイルのある場所を起点にトークンを書き込むため、全員が同じ場所にあるconfigを参照すると全員のトークンも同じ場所に出来てしまう、とか、じゃあそれを何とかするためにその辺のディレクトリのパーミッションをちゃんと全員が書き込めるようにしてやる必要がある、とかで考えることが多く面倒くさくなる。

接続先を指定するためには環境変数には以下の値を指定する。ここでは全ユーザー共通に設定するため `/etc/environment` に書くことにする。

```file:/etc/environment
ST2_AUTH_URL=https://st2.example.com:8443/auth
ST2_API_URL=https://st2.example.com:8443/api
ST2_STREAM_URL=https://st2.example.com:8443/stream
ST2_CACERT=/etc/ssl/st2/st2.crt
```

URLはそれぞれ適切に書き換える。`ST2_CACERT` については後述。この段階で、SSL証明書のエラーが出るが、とりあえず `st2` コマンドは叩けるようになる。


## SSL証明書の設定

StackStormを普通にインストールした場合、リモートからauth/api/streamの各サービスに接続する場合はnginxのリバースプロキシ経由で接続することになる。これはst2-dockerを使った場合も同じ。そしてデフォルトではnginxでSSLが有効化されているのでhttpsでの接続になるが、サーバー側の証明書を適切に設定し、さらにクライアント側でも `ST2_CACERT` を正しく設定しないとSSL関連のwarningが出たりエラーが出たりして鬱陶しい。証明書もSubject Alt Nameが適切に設定されていないとpythonのライブラリがwarningを吐きまくるので、適当に設定してあげる。

証明書に関しては環境によって要件が違う(開発or検証環境だから自己署名証明書でよい、production用だからCAで署名された証明書を使う、等)のでセットアップ先に合わせて設定する必要があるが、ポイントは以下。

- アクセスする時に使うURLをSAN(Subject Alt Name)に含んだ証明書を使ってあげる
- `ST2_CACERT`はトラストアンカーにあるCAで署名されている証明書を使う場合でも **必ず明示的に設定する** 

とりあえずst2-dockerを使っていて自己署名証明書を使っている場合は、まず以下の手順でSAN付きの証明書と秘密鍵を作成し、

```bash
openssl req -x509 -new -newkey rsa:2048 -nodes -keyout /etc/ssl/st2/st2.crt \
 -days 3650 -subj "/CN=localhost" -extensions SAN \
 -config <(cat /etc/pki/tls/openssl.cnf \
         <(printf "[SAN]\nsubjectAltName=DNS:localhost,DNS:st2.example.com")) \
 -out /etc/ssl/st2/st2.key
```

`docker-compose.yml` を編集して上記で作成した証明書・秘密鍵それぞれ `/etc/ssl/st2/{st2.crt, st2.key}` にbind mountしてあげる。そしてホスト側のst2clientの設定では、環境変数 `ST2_CACERT` に証明書のCA、この場合は自己証明書なので `/etc/ssl/st2/st2.crt` を指定してあげれば、SSLエラーが出なくなる。

もしうまく設定できていない場合、以下のようなエラーが出で動かない。

```
ERROR: ("bad handshake: Error([('SSL routines', 'tls_process_server_certificate', 'certificate verify failed')],)",)
```

また `ST2_CACERT` を指定しなかった場合、証明書の検証が無効になるが、以下のwarningが毎回出る。

```
/usr/local/opt/virtualenvs/st2/lib/python2.7/site-packages/requests/packages/urllib3/connectionpool.py:852: InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
  InsecureRequestWarning)
```

ちゃんとトラストアンカーに追加されたCAにサインされた証明書を使う場合でも **`ST2_CACERT`の指定は必須なので要注意。** `ST2_CACERT` を指定しなかった場合は、OS標準のトラストアンカーを使って証明書の検証をするのではなく **完全に証明書検証を無効化してwarningを出す** という動作になる。

この件もIssueがあがっている → [RFE: st2client should verify ssl certs by default](https://github.com/StackStorm/st2/issues/3777)

ちなみに上記Issueにも書いてある通り、warningsはconfigで `ssl_silence_warnings = true` を指定すれば出なくすることも出来る(ドキュメント未記載)。しかし、このパラメータは環境変数で設定することは出来ない。ので、要件(全員が何のファイルも変更する必要がない)を満たすためにはSSL証明書をちゃんと設定する必要があった。


# Caveat

殆ど問題無く使えるのだが、今のところ `stream` に接続する場合にちょっとうまく動かない。例えば `st2 execution tail` 等。ちゃんと実行出来るのだが、ストリームの終了をクライアント側で検知出来ず？にいるようで `Ctrl+C` で強制的に止める必要がある。恐らくだがnginx経由で接続していることに起因しているんじゃないか…と思われる。

`st2 pack install/remove` コマンドもstreamに接続しているようで、最後になってコマンドがずっと終わらない状況に陥る。ただコマンド自体は正常に実行出来ている。気持ち悪いので `st2 pack` ではなく代わりに `st2 run packs.install` 等で直接アクションを呼び出すようにしている。