イベントドリブンな自動化ツール StackStormを試す

イベントドリブンな自動化ツール、StackStormを試してみました。

![stackstorm-logo-header.png](https://qiita-image-store.s3.amazonaws.com/0/16458/151ca42b-1701-fb72-04ea-9ddf4f35886f.png)

もともとはスタートアップ企業だったところを、Brocade社が買収しているようですね。

# TL;DR

Jenkinsと似たようなツールであり、それでいてAPIを通じてより細かく、しかも外部から管理ができるというところがメリットです。
Jenkinsだと、実質JenkinsのGUIで殆どを管理することが前提であり、かつワークフロー定義をJenkinsに任せるしかなく、ちょっとなぁ…という課題を持っている方は、検討してみる価値はありそうです[^1]。

https://stackstorm.com/

<img width="1121" alt="公式サイト" src="https://qiita-image-store.s3.amazonaws.com/0/16458/966b1d61-52d5-ba41-a33c-ed5225865c9a.png">

## メリット

* イベント(ジョブ実行のトリガー)をREST APIにできる
* GUIではなくコマンドで(も)管理できる
    * コマンドで全てを管理できるので、独自のDashboardを作ることも簡単にできそうです。
* ワークフローやアクションをテキストで構成管理できる

## デメリット

* 構成要素が多い
    * JenkinsみたいにJDKとJenkinsのパッケージで一発、とかではなく、ものすごくたくさんのパッケージを入れる必要があります。
    * 従って、障害時はどこで問題が起こっていて、どのデータがどういう状態なのかを、アーキテクチャを理解してちゃんと調べる必要があります。

## このような用途に向く

* ワークフローが多くて複雑、ワークフローを構成したい
* REST APIなどで外部から管理したい
* 監視アラートが出たら何らかのアクションを起こす、とか、ChatOpsでSlackに発言したら何かを発動させる、とかもっと多くのトリガーと連携したい

## このような用途には向かない

* ビルドパイプライン

アプリケーションをビルドするとか、デプロイするとか、テストするとかの用途には向きません。
当然実行するだけならできますが、例えばJenkinsだとJUnitテスト結果の集計とかで綺麗に表示することもできますが、そんな機能はありません。
ビルドパイプラインならJenkinsの方が適切でしょう。

* 小規模サービス

そもそもコンポーネントが多すぎるので、機能的に多分オーバースペックでしょう。

* GUIで全てを管理する

GUIはおまけです。そもそもコマンドとかテキストで構成管理とかPull Requestとかで開発フローを回す気がないのであれば、ほとんどメリットはないでしょう。チームの技術的スキルにもよると思います。
何かあった時にポチポチ画面から情報を見ることの方が気楽だというのであれば、StackStormは適さないように思います。
(※ もちろん、別途管理画面を作りこむのであれば話は別です)

## 不明

* スケーラビリティ/可用性

これだけコンポーネントが分かれているのですし、Workerコンポーネントすらいるのですから、多分問題なさそうな気がしていますが、未調査です。

# StackStormとは

非常に乱暴な言い方をするならば、「Jenkinsのジョブ管理、ワークフロー管理を外部から行い易くしたもの」という理解です。
ワークフローや個別のアクションを自由に定義でき、それらをテキストファイルで管理することができます。
また、トリガをコマンドやAPI、時間など様々なものに定義することもできます。

Jenkinsは素晴らしいツールですが、それ故に「Jenkinsから処理を扱う」ことが必須であり、「Jenkinsを"通して"処理を扱う」ことには若干限界を感じていました。
つまり、外部からREST APIなどを叩いて、Jenkinsの持つワークフローなどを実行したり管理したりしたかったのです。

無償版のCommunity Editionと、有償版のEnterprise Editionがあります。
Enterpriseでは、ワークフローをGUIで作ることができるみたいです。

## 特徴

公式[^2]によると、以下の3つだそうです。

* 障害対応の円滑化
* 改善の自動化
* 継続的開発

以下、自分が感じるメリットです。

* GUIも持っていますが、それはむしろおまけで、APIエンドポイントをトリガーにして様々な処理を行うことができます。
つまり、昨今のマイクロサービスアーキテクチャにも親和性が高く、全てREST APIとして処理を扱うことができます。
* ワークフロー(つまり◯◯だったら××を実行する、次に△△を実行する)ということもできます。
* ワークフローやトリガーをファイルで管理できます。つまり昨今のInfrastructure as Codeとの親和性が高いのです。

# StackStormのアーキテクチャ

公式ドキュメント[^2]に書いてあります。
さすが、様々な処理を行うだけあって、コンポーネントは盛りだくさんです。
![architecture_diagram.jpg](https://qiita-image-store.s3.amazonaws.com/0/16458/7ee9dae1-aeb9-e2ae-499c-e4745b45cca5.jpeg)

* 様々なイベントをきっかけにSensorがそれを受け付ける
* SensorがTriggerへ渡し、Ruleに基づいてActionまたはWorkflowに処理を渡す。渡した処理はMessage Queueに貯める。
* Actionは様々な処理を実行する。外部サービスへ飛ばすことも可能。OpenStackのWorkflowプロジェクトであるMistralを利用することも可能。
* 実行履歴や監査はMongoDBに格納される。 

という感じでしょうか。

インフラの構成もこちら[^3]に紹介されています。

![st2-deployment-big-picture.png](https://qiita-image-store.s3.amazonaws.com/0/16458/a40cb5fb-3259-2cc1-d074-bdb9df137aa8.png)

さっとインストールログを見た感じ、これくらいは入ってます。

* RabbitMQ
* MongoDB
* Python
* erlang (RabbitMQのため)
* Node.js
* hubot
* nginx
* PostgreSQL

今回は試しに使ってみるので1ノードに全部入れていますが、本格的に運用するならそれなりにサーバを分けるなりしないと大変そうです。(そう考えると、むしろJenkinsは(pluginの力もありますが)コンパクトにあれだけの機能が詰まっていて凄い…)

# StackStormをインストールする

 インストール方法も公式[^4]にまとまっています。今回はVagrant上にCentOS7を立てて、その中にインストールするため、以下のコマンドで一発でした。

```bash
curl -sSL https://stackstorm.com/packages/install.sh | bash -s -- --user=st2admin --password=<CHANGEME>
```

`<CHANGEME>`には好きなパスワードを入れてください。
様々なパッケージをインストールするため、それなりに時間がかかります。

`st2ctl` コマンドで、StackStormの各コンポーネントの起動状況を調べることができます。

```bash
# st2ctl status
##### st2 components status #####
st2actionrunner PID: 6690
st2actionrunner PID: 6692
st2actionrunner PID: 6694
st2actionrunner PID: 6696
st2actionrunner PID: 6698
st2actionrunner PID: 6700
st2actionrunner PID: 6702
st2actionrunner PID: 6704
st2actionrunner PID: 6706
st2actionrunner PID: 6708
st2api PID: 6715
st2api PID: 7409
st2stream PID: 6721
st2stream PID: 7412
st2auth PID: 6727
st2auth PID: 7456
st2garbagecollector PID: 6733
st2notifier PID: 6739
st2resultstracker PID: 6745
st2rulesengine PID: 6751
st2sensorcontainer PID: 6757
st2chatops is not running.
mistral-server PID: 6786
mistral-api PID: 6784
mistral-api PID: 7399
mistral-api PID: 7510
```

`st2ctl restart` コマンドで一斉に再起動できますので、もしGUIなどに繋がらない場合は試してみてください。

`st2chatops`が起動していませんが、これは内部で動いているhubotの環境設定を行っていないためなので無視します。

インストール後、80番ポートにアクセスすると、Webの管理画面にアクセスすることができます。

<img width="317" alt="StackStormアーキテクチャ" src="https://qiita-image-store.s3.amazonaws.com/0/16458/c8ff3a2f-3522-2931-aa4a-f844ca23cbbc.png">

インストール時に`st2admin`ユーザ、パスワードは自分が指定したものがありますので、それでログインしてみてください。ログインすると、以下のような画面になるはずです。

<img width="1204" alt="StackStormインフラ構成" src="https://qiita-image-store.s3.amazonaws.com/0/16458/c6a070c0-be30-bf32-0b5a-7b81fbcd9c66.png">

`HISTORY` はこれまでの実行履歴、 `ACTIONS`は個々のアクション、`RULES`は何をきっかけ(Trigger)にして何を行うかという定義が表示されます。GUI上で`ACTION`や`RULES`をいじることも可能です。

# `st2`コマンドで様々な動きを確認する

GUI上でもいろいろできるのですが、最初にお伝えした通り、GUIはおまけだと思っています。本質はコマンドやAPI経由でのアクセスでいろいろとコントロールできることだと思うので、その方向でいろいろ試してみましょう。

ここからは若干リファレンス的になるので、結局どうだったの？ということが気になる方は一番最後をご覧ください。

## Action

個々の処理を定義しています。

### Actionの一覧

```bash
# st2 action list
+---------------------------------+---------+----------------------------------------+
| ref                             | pack    | description                            |
+---------------------------------+---------+----------------------------------------+
| chatops.format_execution_result | chatops | Format an execution result for chatops |
| chatops.post_message            | chatops | Post a message to stream for chatops   |
| chatops.post_result             | chatops | Post an execution result to stream for |
|                                 |         | chatops                                |
| core.announcement               | core    | Action that broadcasts the             |
|                                 |         | announcement to all stream consumers.  |
| core.http                       | core    | Action that performs an http request.  |
| core.local                      | core    | Action that executes an arbitrary      |
(snip)
| st2.rules.list                  | st2     | Retrieve a list of available           |
|                                 |         | StackStorm rules                       |
| st2.sensors.list                | st2     | Retrieve a list of available           |
|                                 |         | StackStorm sensors.                    |
| st2.upload_to_s3                | st2     | Sends collected data to write-only     |
|                                 |         | StackStorm S3 bucket                   |
+---------------------------------+---------+----------------------------------------+
```

様々なものがあります。アクションの詳細を見たいときは、 `st2 action get <ACTION_NAME>`で確認することができます。
概ね、以下のようなものがあります。

* ChatOps系
    * チャットで発言する
* HTTPリクエストを送る
* Linuxコマンドを実行する
* リモートサーバでコマンドを実行する
* メールを送る
* StackStormの設定を変更したりパッケージをインストールしたりする
* StackStorm内部系
    * KVSのデータを操作する(Get/Deleteなど)
    * Actionの実行履歴を見る
    * Ruleの一覧を見る
    * Sensorの一覧を見る

### Actionの詳細

```bash
# st2 action get linux.file_touch
+-------------+---------------------------------------------------------+
| Property    | Value                                                   |
+-------------+---------------------------------------------------------+
| id          | 57b6f08de368b932e541591b                                |
| uid         | action:linux:file_touch                                 |
| ref         | linux.file_touch                                        |
| pack        | linux                                                   |
| name        | file_touch                                              |
| description | Touches a file                                          |
| enabled     | True                                                    |
| entry_point |                                                         |
| runner_type | remote-shell-cmd                                        |
| parameters  | {                                                       |
|             |     "cmd": {                                            |
|             |         "default": "echo $(date +%s) > {{file}}",       |
|             |         "immutable": true                               |
|             |     },                                                  |
|             |     "file": {                                           |
|             |         "required": true,                               |
|             |         "type": "string",                               |
|             |         "description": "Path of the file to be touched" |
|             |     }                                                   |
|             | }                                                       |
| notify      |                                                         |
| tags        |                                                         |
+-------------+---------------------------------------------------------+
```

なお、実体は以下にあります。

```bash
/opt/stackstorm/packs/linux/actions/file_touch.yaml
```

実際にこのアクションを実行するときに、どんなパラメータが必要になるかは`st2 run <ACTION_NAME> -h`で確認できます。

```bash
# st2 run linux.file_touch -h

Touches a file

Required Parameters:
    file
        Path of the file to be touched
        Type: string

    hosts
        A comma delimited string of a list of hosts where the remote command
        will be executed.
        Type: string

Optional Parameters:
    bastion_host
(snip)
```

### Actionの実行

実際にパラメータ込みで実行する際は、 `st2 run <ACITON_NAME> param1=value1 param2=value2 ...` で実行できます。

```
# st2 run linux.file_touch file="/tmp/test" hosts="192.168.33.11" username="vagrant" password="vagrant"
...
id: 57b98242e368b91cf1cf646f
status: succeeded
parameters:
  file: /tmp/test
  hosts: 192.168.33.11
  password: '********'
  username: vagrant
result:
  192.168.33.11:
    failed: false
    return_code: 0
    stderr: ''
    stdout: ''
    succeeded: true
```

ここでは、リモートサーバである`192.168.33.11`のサーバに`vagrant`ユーザでアクセスし、`linux.touch`アクションを実行しています。

実際、リモートサーバ側で見てみると…

```bash
$ ll /tmp/test
-rw-rw-r--. 1 vagrant vagrant 11  8月 21 06:28 /tmp/test
$ cat /tmp/test
1471775301
```

おお、できている。

### Actionの実行履歴

ACTIONの実行結果は、あとで`st2 execution list`コマンドで確認することもできます。

```
# st2 execution list
+----------------------------+----------------+--------------+-------------------------+-----------------+---------------+
| id                         | action.ref     | context.user | status                  | start_timestamp | end_timestamp |
+----------------------------+----------------+--------------+-------------------------+-----------------+---------------+
(snip)
|   57b98242e368b91cf1cf646f | linux.file_tou | st2admin     | succeeded (4s elapsed)  | Sun, 21 Aug     | Sun, 21 Aug   |
|                            | ch             |              |                         | 2016 10:28:18   | 2016 10:28:22 |
|                            |                |              |                         | UTC             | UTC           |
+----------------------------+----------------+--------------+-------------------------+-----------------+---------------+
```

### Actionの実行履歴の詳細

`st2 execution get <ID>` コマンドで`id`を指定すれば、その時の結果を見ることもできます。

```
# st2 execution get 57b98242e368b91cf1cf646f
id: 57b98242e368b91cf1cf646f
status: succeeded (4s elapsed)
parameters:
  file: /tmp/test
  hosts: 192.168.33.11
  password: '********'
  username: vagrant
result:
  192.168.33.11:
    failed: false
    return_code: 0
    stderr: ''
    stdout: ''
    succeeded: true
```

## Action Runner

Action Runnerは、Actionを実行するための元となるモジュールというイメージです。

* ローカルコマンド実行
* リモートサーバでのコマンド実行
* Windowsサーバでのコマンド実行
* Pythonスクリプトの実行

などの、実行の「手段」を定義しています。先ほどの`linux.file_touch`アクションでは、以下のように指定されていました。

```
| runner_type | remote-shell-cmd                                        |
```

これらも、 `st2 runner list` コマンドで一覧、 `st2 runner get <RUNNER_NAME>` で詳細を見ることができますが、詳細は割愛します。割と十分な数が用意されているように見えますが、例えば`Rubyを実行`というRunnerを新たに追加することもできそうです。

## Rule

RuleはIFTTTみたいなもので、「何が起きたら(Trigger)」「どんな条件のときに(Criteria)」「何をする(Action)」ということを定義するものです。

今回はせっかくなので、APIによってActionを発動させてみましょう。

流れとしては、以下のようになります。

1. trigger, criteria, action を定義するymlファイルを作る
2. コマンドによってStackStormに反映

### Triggerを確認する

既に定義されているTriggerを見てみましょう。`st2 trigger list`コマンドで見ることができます。

```bash
+--------------------------------------+-------+--------------------------------------------------+
| ref                                  | pack  | description                                      |
+--------------------------------------+-------+--------------------------------------------------+
| core.st2.generic.actiontrigger       | core  | Trigger encapsulating the completion of an       |
|                                      |       | action execution.                                |
| core.st2.IntervalTimer               | core  | Triggers on specified intervals. e.g. every 30s, |
|                                      |       | 1week etc.                                       |
| core.st2.generic.notifytrigger       | core  | Notification trigger.                            |
| core.st2.action.file_writen          | core  | Trigger encapsulating action file being written  |
|                                      |       | on disk.                                         |
| core.st2.key_value_pair.create       | core  | Trigger encapsulating datastore item creation.   |
| core.st2.key_value_pair.update       | core  | Trigger encapsulating datastore set action.      |
(snip)
| linux.file_watch.line                | linux | Trigger which indicates a new line has been      |
|                                      |       | detected                                         |
| core.st2.webhook                     | core  | Trigger type for registering webhooks that can   |
|                                      |       | consume arbitrary payload.                       |
+--------------------------------------+-------+--------------------------------------------------+
```

概ね以下のようなtriggerがあります。

* 時間系
    * 定期的な間隔で実行
    * 定期的な時刻で実行
    * 指定された時刻で実行
* KVS系
    * Keyが作成/削除/更新されたら実行
* 指定されたファイルを監視して、新しい行が増えたら実行
* Webhookで特定のURLにリクエストが来たら実行

特に一番最後のが便利そうですね。

### 定義YAMLの作成

まず、定義用のymlとして`test.yml`を作成します。

```yaml:test.yml
---
    name: "sample_rule"
    pack: "test"
    description: "Test Rule 1"
    enabled: true

    trigger:
        type: "core.st2.webhook"
        parameters:
          url: "test1"

    action:
        ref: "core.local"
        parameters:
            cmd: "echo {{ trigger }}"
```

ここでは単純に、criteriaをなくして(つまりAPIにアクセスが来たら無条件に)actionを実行する形にしています。
また、`{{ trigger }}`として、triggerから受け取る値を表示させるようにしてみます。

作成したら、`st2 rule create <YAMLファイル>` コマンドで反映させます。

```bash
# st2 rule create test.yml
+-------------+----------------------------------------------------------+
| Property    | Value                                                    |
+-------------+----------------------------------------------------------+
| id          | 57b99fece368b91cf1cf6475                                 |
| name        | sample_rule                                              |
| pack        | test                                                     |
| description | Test Rule 1                                              |
| action      | {                                                        |
|             |     "ref": "core.local",                                 |
|             |     "parameters": {                                      |
|             |         "cmd": "echo {{ trigger }}"                      |
|             |     }                                                    |
|             | }                                                        |
| criteria    |                                                          |
| enabled     | True                                                     |
| ref         | test.sample_rule                                         |
| tags        |                                                          |
| trigger     | {                                                        |
|             |     "type": "core.st2.webhook",                          |
|             |     "ref": "core.4e1bc9d3-7331-47ee-a894-6e68314635a3",  |
|             |     "parameters": {                                      |
|             |         "url": "test1"                                   |
|             |     }                                                    |
|             | }                                                        |
| type        | {                                                        |
|             |     "ref": "standard",                                   |
|             |     "parameters": {}                                     |
|             | }                                                        |
| uid         | rule:test:sample_rule                                    |
+-------------+----------------------------------------------------------+
```

作成できました。
なお、一度作ったRuleを更新する場合は、`st2 rule create`コマンドではなく、`st2 rule update <ID> <YAMLファイル>`コマンドで反映させます。IDは、上記の通り表示されています。(`57b99fece368b91cf1cf6475`のことです)

作成したruleは、`st2 rule list`コマンドで確認できます。

```bash
# st2 rule list
+-------------------+---------+--------------------------------------+---------+
| ref               | pack    | description                          | enabled |
+-------------------+---------+--------------------------------------+---------+
| chatops.notify    | chatops | Notification rule to send results of | True    |
|                   |         | action executions to stream for      |         |
|                   |         | chatops                              |         |
| test.sample_rule  | test    | Test Rule 1                          | True    |
+-------------------+---------+--------------------------------------+---------+
```

chatops.notifyはデフォルトで入ってるものです。2つめがちゃんと入っていますね。

また、webhookの設定にしているため、webhookとしての設定にも反映されています。
webhookのリストは`st2 webhook list`コマンドで表示することができます。

```bash
# st2 webhook list
+------------------+------+-------------------------------+-------------+--------------------+
| type             | pack | name                          | description | parameters         |
+------------------+------+-------------------------------+-------------+--------------------+
| core.st2.webhook | core | 4e1bc9d3-7331-47ee-a894-6e683 |             | {u'url': u'test1'} |
|                  |      | 14635a3                       |             |                    |
+------------------+------+-------------------------------+-------------+--------------------+
```

`test1`というURLエンドポイントが認識されています。

### 認証トークンの作成

さて、いよいよ`curl`コマンドで…と行きたいところですが、この時点で実行しても失敗します。

```bash
$ curl -k https://localhost/api/v1/webhooks/test1
{
    "faultstring": "Unauthorized - One of Token or API key required."
}
```

実行には認証トークンが必要です。
トークンの作成には、`st2 auth -t <ユーザ名>`コマンドを実行します。`-p <パスワード>`を追加すると一度に作成できます。適当に環境変数に入れておくとよいでしょう。

以下の例は、`id: st2admin, password: st2admin` の例です。

```bash
$ ST2_AUTH_TOKEN=$(st2 auth -t -p st2admin st2admin)
$ echo $ST2_AUTH_TOKEN
4dec06213e094eb2838d0347f4cdd509
```

### APIを叩いてRuleの発動を確認

さて、いよいよ確認です。先ほど取得したトークンも入れて、実行してみましょう。適当にRequest Bodyに値も入れておきます。

```bash
$ curl -XPOST -k https://localhost/api/v1/webhooks/test1 -H "X-Auth-Token: $ST2_AUTH_TOKEN" -H "Content-Type: application/json" -d '{"key1":"value1"}'
{"key1": "value1"}
```

Triggerが動いたとすると、StackStormとしては`trigger-instance`という形で履歴が残ります。いわば、Triggerの実行履歴ということです。

実行履歴の確認は、`st2 trigger-instance list`コマンドで分かります。

```bash
# st2 trigger-instance list
+--------------------------+--------------------------------+-------------------------------+-----------+
| id                       | trigger                        | occurrence_time               | status    |
+--------------------------+--------------------------------+-------------------------------+-----------+
(snip)
| 57b9a7e9e368b91a5fb67434 | core.st2.generic.actiontrigger | Sun, 21 Aug 2016 13:08:57 UTC | processed |
+--------------------------+--------------------------------+-------------------------------+-----------+
```

詳細は、上記のidを利用して`st2 trigger-instance get <ID>`コマンドで分かります。

```bash
# st2 trigger-instance get 57b9a7e9e368b91a5fb67434
+-----------------+--------------------------------------------------------------+
| Property        | Value                                                        |
+-----------------+--------------------------------------------------------------+
| id              | 57b9a7e9e368b91a5fb67434                                     |
| trigger         | core.st2.generic.actiontrigger                               |
| occurrence_time | 2016-08-21T13:08:57.589000Z                                  |
| payload         | {                                                            |
|                 |     "status": "succeeded",                                   |
|                 |     "start_timestamp": "2016-08-21 13:08:54.532719+00:00",   |
|                 |     "parameters": {                                          |
|                 |         "cmd": "echo {'body': {'key1': 'value1'}, 'headers': |
|                 | {'X-Request-Id': 'a76e0532-362a-4983-bc16-f29fe6804d4a', 'X  |
|                 | -Forwarded-For': '127.0.0.1', 'Content-Length': '17',        |
|                 | 'Accept': '*/*', 'User-Agent': 'curl/7.29.0', 'Host':        |
|                 | 'localhost,localhost', 'X-Real-Ip': '127.0.0.1', 'X-Auth-    |
|                 | Token': '4dec06213e094eb2838d0347f4cdd509', 'Content-Type':  |
|                 | 'application/json'}}"                                        |
|                 |     },                                                       |
|                 |     "runner_ref": "local-shell-cmd",                         |
|                 |     "action_name": "core.local",                             |
|                 |     "result": {                                              |
|                 |         "succeeded": true,                                   |
|                 |         "failed": false,                                     |
|                 |         "return_code": 0,                                    |
|                 |         "stderr": "",                                        |
|                 |         "stdout": "{body: {key1: value1}, headers: {X        |
|                 | -Request-Id: a76e0532-362a-4983-bc16-f29fe6804d4a, X         |
|                 | -Forwarded-For: 127.0.0.1, Content-Length: 17, Accept: */*,  |
|                 | User-Agent: curl/7.29.0, Host: localhost,localhost, X-Real-  |
|                 | Ip: 127.0.0.1, X-Auth-Token:                                 |
|                 | 4dec06213e094eb2838d0347f4cdd509, Content-Type:              |
|                 | application/json}}"                                          |
|                 |     },                                                       |
|                 |     "action_ref": "core.local",                              |
|                 |     "execution_id": "57b9a7e6e368b91a5fb67432"               |
|                 | }                                                            |
| status          | processed                                                    |
+-----------------+--------------------------------------------------------------+
```

どうやら動いているようです！

`stdout`にも以下のように出力されている通り、`trigger`という変数にはheaderとbodyが格納されているようです。

```
{body: {key1: value1}, headers: {X-Request-Id: a76e0532-362a-4983-bc16-f29fe6804d4a, X-Forwarded-For: 127.0.0.1, Content-Length: 17, Accept: */*, User-Agent: curl/7.29.0, Host: localhost,localhost, X-Real-Ip: 127.0.0.1, X-Auth-Token: 4dec06213e094eb2838d0347f4cdd509, Content-Type: application/json}}
```

先ほどのコマンドで実行を確認しましたが、actionとしても動いた履歴が蓄積されるので、`execution_id`を元にaction側の履歴も拾うことができます。

```bash
# st2 execution get 57b9a7e6e368b91a5fb67432
id: 57b9a7e6e368b91a5fb67432
status: succeeded (3s elapsed)
parameters:
  cmd: 'echo {''body'': {''key1'': ''value1''}, ''headers'': {''X-Request-Id'': ''a76e0532-362a-4983-bc16-f29fe6804d4a'', ''X-Forwarded-For'': ''127.0.0.1'', ''Content-Length'': ''17'', ''Accept'': ''*/*'', ''User-Agent'': ''curl/7.29.0'', ''Host'': ''localhost,localhost'', ''X-Real-Ip'': ''127.0.0.1'', ''X-Auth-Token'': ''4dec06213e094eb2838d0347f4cdd509'', ''Content-Type'': ''application/json''}}'
result:
  failed: false
  return_code: 0
  stderr: ''
  stdout: '{body: {key1: value1}, headers: {X-Request-Id: a76e0532-362a-4983-bc16-f29fe6804d4a, X-Forwarded-For: 127.0.0.1, Content-Length: 17, Accept: */*, User-Agent: curl/7.29.0, Host: localhost,localhost, X-Real-Ip: 127.0.0.1, X-Auth-Token: 4dec06213e094eb2838d0347f4cdd509, Content-Type: application/json}}'
  succeeded: true
```

確かに動いてますね。

ここでは紹介しませんが、Ruleの事前テストもできるようです。
ルールを作ったけどうまく発動しないことを防ぐこともできますし、継続的インテグレーションもしやすそうですね。

https://docs.stackstorm.com/rules.html#testing-rules


### rule応用編

もう少し応用的なものを作ってみます。

```yaml:test2.yml
---
    name: "sample_rule2"
    pack: "test"
    description: "Test Rule 2"
    enabled: true

    trigger:
        type: "core.st2.webhook"
        parameters:
          url: "test2"

    criteria:
        trigger.body.key1:
            type: "regex"
            pattern : "^value[1-3]$"

    action:
        ref: "core.remote"
        parameters:
            hosts: "192.168.33.11"
            username: "vagrant"
            password: "vagrant"
            cmd: "echo {{ trigger.body.key1 }} >> /tmp/test.log"
```

今度は、criteriaを定義しました。また、actionにはリモートサーバでのコマンド実行と、さらにbodyの`key1`を拾うようにしています。

作成したら、反映します。

```bash
# st2 rule create test2.yml
+-------------+--------------------------------------------------------------+
| Property    | Value                                                        |
+-------------+--------------------------------------------------------------+
| id          | 57b9ac3de368b91cf1cf6480                                     |
| name        | sample_rule2                                                 |
| pack        | test                                                         |
| description | Test Rule 2                                                  |
| action      | {                                                            |
|             |     "ref": "core.remote",                                    |
|             |     "parameters": {                                          |
|             |         "username": "vagrant",                               |
|             |         "password": "vagrant",                               |
|             |         "cmd": "echo {{ trigger.body.key1 }} >>              |
|             | /tmp/test.log",                                              |
|             |         "hosts": "192.168.33.11"                             |
|             |     }                                                        |
|             | }                                                            |
| criteria    | {                                                            |
|             |     "trigger.body.key1": {                                   |
|             |         "pattern": "^value[1-3]$",                           |
|             |         "type": "regex"                                      |
|             |     }                                                        |
|             | }                                                            |
| enabled     | True                                                         |
| ref         | test.sample_rule2                                            |
| tags        |                                                              |
| trigger     | {                                                            |
|             |     "type": "core.st2.webhook",                              |
|             |     "ref": "core.1d55ccf0-15ae-4194-9dc4-d39d7c35bbc2",      |
|             |     "parameters": {                                          |
|             |         "url": "test2"                                       |
|             |     }                                                        |
|             | }                                                            |
| type        | {                                                            |
|             |     "ref": "standard",                                       |
|             |     "parameters": {}                                         |
|             | }                                                            |
| uid         | rule:test:sample_rule2                                       |
+-------------+--------------------------------------------------------------+
```

さて、わざとcriteriaに引っかからない条件でPOSTしてみましょう。

```bash
$ curl -XPOST -k https://localhost/api/v1/webhooks/test2 -H "X-Auth-Token: $ST2_AUTH_TOKEN" -H "Content-Type: application/json" -d '{"key1":"value0"}'
```

trigger-instanceで最新のもの(`-n1`)を確認してみます。

```bash
# st2 trigger-instance list -n 1
+--------------------------+--------------------------------+-------------------------------+-----------+
| id                       | trigger                        | occurrence_time               | status    |
+--------------------------+--------------------------------+-------------------------------+-----------+
| 57b9ad04e368b91a5fb67435 | core.1d55ccf0-15ae-4194-9dc4-d | Sun, 21 Aug 2016 13:30:44 UTC | processed |
|                          | 39d7c35bbc2                    |                               |           |
+--------------------------+--------------------------------+-------------------------------+-----------+
```

```bash
# st2 trigger-instance get 57b9ad04e368b91a5fb67435
+-----------------+--------------------------------------------------------------+
| Property        | Value                                                        |
+-----------------+--------------------------------------------------------------+
| id              | 57b9ad04e368b91a5fb67435                                     |
| trigger         | core.1d55ccf0-15ae-4194-9dc4-d39d7c35bbc2                    |
| occurrence_time | 2016-08-21T13:30:44.078000Z                                  |
| payload         | {                                                            |
|                 |     "body": {                                                |
|                 |         "key1": "value0"                                     |
|                 |     },                                                       |
|                 |     "headers": {                                             |
|                 |         "X-Request-Id": "c369cfaf-d780-4e10-997b-            |
|                 | a3cd48dac406",                                               |
|                 |         "X-Forwarded-For": "127.0.0.1",                      |
|                 |         "Content-Length": "17",                              |
|                 |         "Accept": "*/*",                                     |
|                 |         "User-Agent": "curl/7.29.0",                         |
|                 |         "Host": "localhost,localhost",                       |
|                 |         "X-Real-Ip": "127.0.0.1",                            |
|                 |         "X-Auth-Token": "4dec06213e094eb2838d0347f4cdd509",  |
|                 |         "Content-Type": "application/json"                   |
|                 |     }                                                        |
|                 | }                                                            |
| status          | processed                                                    |
+-----------------+--------------------------------------------------------------+
```

trigger-instanceとしては動いているようですが…actionが見当たりません。
action側の最新のものを見てみます。

```bash
# st2 execution list -n1 # -> さっきと同じ結果…つまりactionが動いてない。criteriaが効いている証拠。
+--------------------------+------------+--------------+------------------------+----------------------+------------------+
| id                       | action.ref | context.user | status                 | start_timestamp      | end_timestamp    |
+--------------------------+------------+--------------+------------------------+----------------------+------------------+
| 57b9a7e6e368b91a5fb67432 | core.local | stanley      | succeeded (3s elapsed) | Sun, 21 Aug 2016     | Sun, 21 Aug 2016 |
|                          |            |              |                        | 13:08:54 UTC         | 13:08:57 UTC     |
+--------------------------+------------+--------------+------------------------+----------------------+------------------+
```

先ほどと同じものがでてきました。つまり、actionが動いていないということになります。ちゃんと、criteriaが効いているということですね。

それでは、今度は「ちゃんと」動かしてみます。

```
$ curl -XPOST -k https://localhost/api/v1/webhooks/test2 -H "X-Auth-Token: $ST2_AUTH_TOKEN" -H "Content-Type: application/json" -d '{"key1":"value1"}'
{"key1": "value1"}
```

以下の通り、今度はちゃんと動いています。


```
# st2 trigger-instance list -n 1
+--------------------------+--------------------------------+-------------------------------+-----------+
| id                       | trigger                        | occurrence_time               | status    |
+--------------------------+--------------------------------+-------------------------------+-----------+
| 57b9aef8e368b91a5fb67442 | core.st2.generic.actiontrigger | Sun, 21 Aug 2016 13:39:04 UTC | processed |
+--------------------------+--------------------------------+-------------------------------+-----------+
```

```
# st2 trigger-instance get 57b9aef8e368b91a5fb67442
+-----------------+-------------------------------------------------------------+
| Property        | Value                                                       |
+-----------------+-------------------------------------------------------------+
| id              | 57b9aef8e368b91a5fb67442                                    |
| trigger         | core.st2.generic.actiontrigger                              |
| occurrence_time | 2016-08-21T13:39:04.173000Z                                 |
| payload         | {                                                           |
|                 |     "status": "succeeded",                                  |
|                 |     "start_timestamp": "2016-08-21 13:39:01.239573+00:00",  |
|                 |     "parameters": {                                         |
|                 |         "username": "vagrant",                              |
|                 |         "password": "********",                             |
|                 |         "cmd": "echo value1 >> /tmp/test.log",              |
|                 |         "hosts": "192.168.33.11"                            |
|                 |     },                                                      |
|                 |     "runner_ref": "remote-shell-cmd",                       |
|                 |     "action_name": "core.remote",                           |
|                 |     "result": {                                             |
|                 |         "192.168.33.11": {                                  |
|                 |             "failed": false,                                |
|                 |             "stderr": "",                                   |
|                 |             "return_code": 0,                               |
|                 |             "succeeded": true,                              |
|                 |             "stdout": ""                                    |
|                 |         }                                                   |
|                 |     },                                                      |
|                 |     "action_ref": "core.remote",                            |
|                 |     "execution_id": "57b9aef5e368b91a5fb67440"              |
|                 | }                                                           |
| status          | processed                                                   |
+-----------------+-------------------------------------------------------------+
```

```
# st2 execution list -n1
+--------------------------+-------------+--------------+------------------------+----------------------+------------------+
| id                       | action.ref  | context.user | status                 | start_timestamp      | end_timestamp    |
+--------------------------+-------------+--------------+------------------------+----------------------+------------------+
| 57b9aef5e368b91a5fb67440 | core.remote | stanley      | succeeded (3s elapsed) | Sun, 21 Aug 2016     | Sun, 21 Aug 2016 |
|                          |             |              |                        | 13:39:01 UTC         | 13:39:04 UTC     |
+--------------------------+-------------+--------------+------------------------+----------------------+------------------+
```

実際、リモートサーバ側の`/tmp/test.log`を見ても、ちゃんと動いていることが分かります。

```bash:192.168.33.11の/tmp/test.log
$ cat /tmp/test.log
value1
```

ちゃんと値も取れていますね。

## Workflow

ここまでで既にあまりに長いので、一旦ここまでにします。RuleによってTriggerとActionは紐付けることができたのですが、実はActionは1つしか定義できません。
ですので、Triggerを条件に複数の処理を動かす場合、必然的にWorkflowを使うことになります。

## その他ハック的なもの

### 出力をtable形式から変更できないの？

`-j`で`json`、`-y`で`yaml`、`-a ATTR`で出力を絞ることができます。

```bash:json形式
# st2 webhook list -j
[
    {
        "name": "4e1bc9d3-7331-47ee-a894-6e68314635a3",
        "pack": "core",
        "parameters": {
            "url": "test1"
        },
        "type": "core.st2.webhook"
    }
]
```

```bash:yaml形式にして更に出力を絞る
# st2 webhook list -y -a type parameters.url
-   parameters:
        url: test1
    type: core.st2.webhook
```

### 出力を絞り込みたい

例えば最後の一つだけ表示したいときは、`-n1`を付与すればよさそうです。

```
# st2 execution list -n 1
+--------------------------+------------+--------------+------------------------+----------------------+------------------+
| id                       | action.ref | context.user | status                 | start_timestamp      | end_timestamp    |
+--------------------------+------------+--------------+------------------------+----------------------+------------------+
| 57b9a7e6e368b91a5fb67432 | core.local | stanley      | succeeded (3s elapsed) | Sun, 21 Aug 2016     | Sun, 21 Aug 2016 |
|                          |            |              |                        | 13:08:54 UTC         | 13:08:57 UTC     |
+--------------------------+------------+--------------+------------------------+----------------------+------------------+
```

他 `-s`でソートキー、`-tg` `-tl`で時間の幅を選ぶこともできます。

### APIでどこまでできるか

実は、今までの`st2`コマンドは、全てAPIに問い合わせに行っています。

これは、`st2`コマンドに`--debug`オプションを付与すると分かります。
例えば、`st2 action list` コマンドに付与した場合:

```bash
$ st2 --debug action list
# -------- begin 25107472 request ----------
curl -X GET -H  'Connection: keep-alive' -H  'Accept-Encoding: gzip, deflate' -H  'Accept: */*' -H  'User-Agent: python-requests/2.10.0' -H  'X-Auth-Token: 366aa51f916d4d70b370178f042f8ca5' http://127.0.0.1:9101/v1/actions
# -------- begin 25107472 response ----------
[{"name": "format_execution_result", "parameters": {"execution_id": {"required": true, "type": "string", "description": "Id of execution to format"}}, (snip)
```

ということで、ご丁寧に`curl`コマンドまで見せてくれます。

```bash
$ curl -sk https://localhost/api/v1/rules  -H "X-Auth-Token: $ST2_AUTH_TOKEN" -H "Content-Type: application/json" | jq "."
[
  {
    "description": "Notification rule to send results of action executions to stream for chatops",
    "tags": [],
    "type": {
      "ref": "standard",
      "parameters": {}
    },
    "enabled": true,
    "name": "notify",
    "trigger": {
      "ref": "core.st2.generic.notifytrigger",
      "type": "core.st2.generic.notifytrigger",
      "parameters": {}
    },
    "criteria": {
(snip)
```

ということで、Webhookのトリガー操作だけでなく、StackStormに対する操作も全てAPI経由にできるという感動。

### 所感

* `st2` コマンドはかなり直感的で使いやすかったです。 `list`を叩けば大抵の一覧、 `get <引数>`を叩けば詳細が見えます。その他、出力の整形やフィルタリングも、オプションはほぼ共通なので迷わない。これはかなり便利でした。
* ワークフローの定義がコマンドとテキストファイルベースで管理できるので、GitHubなどの構成管理と組み合わせるとかなり強力そうです。
* REST APIとしての連携も柔軟にできるので、マイクロサービスアーキテクチャとしても使いやすいのではないでしょうか。
* あらかじめ定義されているものでも十分ですが、もし足りなければActionもRuleもTriggerも好きに作ることができます。Action Runnerがある通り、言語すら自由にできます(OSとしては解釈できなければダメでしょうけれども)。
* どうでもいいですが、StackStormを`st2`と表現するのがちょっと好きです。

個人的には、なんで今までこれを知らなかったんだ…というくらい便利でした。

次回、ワークフローについても調べて紹介します。

# 参考

* [イベントドリブンな StackStorm で運用自動化 | ジェダイさんのブログ](https://jedipunkz.github.io/blog/2016/07/02/stackstorm/)


[^1]: もちろんJenkinsでもできるという反論はそうですし、そもそもJenkinsと比べること自体どうなのというツッコミもあると思いますが、自分が調査するモチベーションがそもそも「Jenkinsの代替となるか」という観点だったので、その点はご容赦ください。
[^2]: https://docs.stackstorm.com/overview.html#about
[^3]: https://docs.stackstorm.com/install/overview.html
[^4]: https://docs.stackstorm.com/install/index.html#installation