StackStormでWorkflowを使ってみる

前回、StackStormの様々な機能について見てきました。

[イベントドリブンな自動化ツール StackStormを試す
](http://qiita.com/unchemist/items/0e6425396ba1a1e6b43c)

そこで残していたWorkflowを試してみます。

## Workflowとは

様々な単体のActionを組み合わせて複数の処理を動かすための仕組みです。
StackStormにとっては、実現する方法が何種類か用意されています。

上に行くほどシンプル(ただし混みいったことはできない)、下に行くほど高機能(ただし書き方が複雑になる)という感じです。

* ActionChain
* Mistral
* Mistral + YAQL

なお、他にCloudSlangという形式もあるようですが、Experimentalということで調査していません。

https://docs.stackstorm.com/workflows.html

### ActionChain

StackStormオリジナルの記法で、速さとシンプルさを求めるならこれ(とのこと)。
ただし、見た感じは、単純にActionを繋げることしかできません。
つまり、Actionを並列で実行したりとかはできません。

### Mistral

OpenStackのWorkflowプロジェクトであるMistralが使えるようです。
つまり、Workflowの書き方はMistralに完全に倣っている感じです。

Workflowの中にWorkflowとか、Actionを分岐させたり合流させたり、あるいはエラーハンドリングを行うということができます。

Mistral:
http://docs.openstack.org/developer/mistral/dsl/dsl_v2.html

### Mistral + YAQL

Mistralに加えて、変数に基づいてActionを組み立てることができます。
例えば配列形式のinputを与えて、その配列の単位でActionを繰り返し実行するとか、
よりプログラマブルにWorkflowを構成できるようです。

YAQLは、`Yet Another Query Language` の略で、いわばJSONにおける `jq` コマンドのようなものの理解です。
つまり、YAMLやJSON形式のコードに対して、必要な要素を抽出したり加工したりすることができます。

Mistral + YAQLでは、YAMLやJSONを加工して値を取り出したりして、その値に基づいてWorkflowを組み立てることができます。
Ansibleを知っている人なら、 `with_items` とか `with_dict` みたいな感じでtaskを回せる、というと一発でイメージが湧くかと思います。

YAQL: 
http://yaql.readthedocs.io/en/latest/getting_started.html  
YAQLuator(YAQLを試すことができるサービス):
http://yaqluator.com/

### どのWorkflowを使うべきか

このように、Workflowを使うためには様々な実現手段があることが分かりました。
なら、どれを使えばよいかですが、正直ActionChainはやりたいことにすぐ限界が来そうですし、
ここはMistralを覚えてしまいたいと思います。

## Mistralを使う

### 認証

前回はrootユーザで実行していたのですが、通常 `st2` コマンドを実行するためには、認証を行う必要があるようです。

https://docs.stackstorm.com/authentication.html?highlight=user

```bash
st2 auth st2admin -p <password> -t
```

`-t`オプションで、トークンのみを表示することができます。
これを、`ST2_AUTH_TOKEN` という環境変数に設定すれば、コマンドを実行できるようになります。

```bash
$ st2 action list
ERROR: 401 Client Error: Unauthorized
MESSAGE: Unauthorized - One of Token or API key required. for url: http://127.0.0.1:9101/v1/actions

$ export ST2_AUTH_TOKEN=$(st2 auth st2admin -p st2admin -t)
$ 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                               |
(snip)
```

### WorkflowとActionの作成と配置

Workflowは、Actionとセットで紐付きます。
WorkflowにもActionにもpack名という接頭辞がつき、この名前でグルーピングが行われます。
ディレクトリ構成も固定的のようです。

#### `/opt/stackstorm/packs/<pack>/actions/*.yml`  

metadataを配置します。どのworkflowを読み込むかとか、どんなパラメータが必要かなど、全体的な設定を書きます。

```/opt/stackstorm/packs/test/actions/mistral-basic.yaml
---
description: Run a local linux command
enabled: true
entry_point: workflows/mistral-basic.yaml
name: mistral-basic
pack: test
parameters:
  cmd:
    required: true
    type: string
runner_type: mistral-v2
```

なんとなく分かると思いますが、主に以下のような定義を行っています。

* `cmd`というパラメータは実行時に入力必須
* `Workflowを `mistral-v2` で動かす

#### `/opt/stackstorm/packs/<pack>/actions/workflows/*.yml`  

Workflowを配置します。具体的な処理の中身を書きます。

```yml:/opt/stackstorm/packs/test/actions/workflows/mistral-basic.yaml
version: '2.0'

test.mistral-basic:
    description: A basic workflow that runs an arbitrary linux command.
    type: direct
    input:
        - cmd
    output:
        stdout: <% $.stdout %>
    tasks:
        task1:
            action: core.local cmd=<% $.cmd %>
            publish:
                stdout: <% task(task1).result.stdout %>
                stderr: <% task(task1).result.stderr %>
```

必ず`/opt/stackstorm/pack`に置かなければなりません。反映時にエラーになります。
配置先を変更する場合は、 `/etc/st2/st2.conf` に以下の記述を行う必要があるようです(未確認)[^1]。

```
[content]
packs_base_paths=/home/myuser1/packs:/home/myuser2/packs
```

配置後、ジョブを反映させます。

```bash
$ st2 action create /opt/stackstorm/packs/test/actions/mistral-basic.yaml
(snip)
```

### Workflowの実行

Actionを実行する時と同じく、 `st2 run <workflow名>` でできます。

`cmd`を入力必須にしたので、何も与えないとエラーになります。

```bash
$ st2 run test.mistral-basic
ERROR: 400 Client Error: Bad Request
MESSAGE: 'cmd' is a required property for url: http://127.0.0.1:9101/v1/executions
```

また、普通に実行すると完了までプロンプトが戻ってきませんが、`-a`オプションを付与すると、非同期で実行できます。

```
$ st2 run test.mistral-basic cmd=date -a
To get the results, execute:
 st2 execution get 57bc7191e368b9124ddab4da
```

しかもご丁寧に、結果を見るときはこのコマンドを叩けと教えてくれるという。
叩くと、以下の通り結果が分かります。

```
$ st2 execution get 57bc7191e368b9124ddab4da
id: 57bc7191e368b9124ddab4da
action.ref: test.mistral-basic
parameters:
  cmd: date
status: succeeded (6s elapsed)
start_timestamp: 2016-08-23T15:53:53.347298Z
end_timestamp: 2016-08-23T15:53:59.271906Z
+--------------------------+------------------------+-------+------------+-------------------------------+
| id                       | status                 | task  | action     | start_timestamp               |
+--------------------------+------------------------+-------+------------+-------------------------------+
| 57bc7194e368b9124ddab4dd | succeeded (1s elapsed) | task1 | core.local | Tue, 23 Aug 2016 15:53:56 UTC |
+--------------------------+------------------------+-------+------------+-------------------------------+
```

もちろん、さらに配下のActionの詳細も分かります。

```
$ st2 execution get 57bc7194e368b9124ddab4dd
id: 57bc7194e368b9124ddab4dd
status: succeeded (1s elapsed)
parameters:
  cmd: date
result:
  failed: false
  return_code: 0
  stderr: ''
  stdout: Tue Aug 23 11:53:57 EDT 2016
  succeeded: true
```

### さらに複雑なWorkflow

もう少し複雑なWorkflowを作ってみます。

```/opt/stackstorm/packs/test/actions/mistral-advance.yaml
---
description: Run a complex workflow
enabled: true
entry_point: workflows/mistral-advance.yaml
name: mistral-advance
pack: test
parameters:
  param:
    required: true
    type: string
runner_type: mistral-v2
```

```/opt/stackstorm/packs/test/actions/workflows/mistral-advance.yaml
version: "2.0"
name: test.mistral-advance
description: A sample workflow that demonstrates nested workflows, forks, and join.

workflows:

  main:
    type: direct
    input:
      - param
    output:
      output1: <% $.task1_result %>
      output2: <% $.task2_result %>
      output3: <% $.task3_result %>
      output4: <% $.task4_result %>

    tasks:
      task1:
        action: core.local
        input:
          cmd: "sleep 1; echo '$.param: <% $.param %>'"
        publish:
          task1_pub1: "task1_publish1"
          task1_pub2: "task1_publish2"
          task1_result: <% task(task1).result.stdout %>
        on-success:
          - task2
          - task3

      task2:
        action: core.local
        input:
          cmd: "sleep 2; echo '$.param: <$.param %>, $.task1_pub1: <% $.task1_pub1 %>, $.task1_result: <% $.task1_result %>'"
        publish:
          task2_pub1: "task2_publish2"
          task2_result: <% task(task2).result.stdout %>
        on-success:
          - task4

      task3:
        action: core.local
        input:
          cmd: "sleep 4; echo '$.param: <$.param %>, $.task1_pub1: <% $.task1_pub1 %>'"
        publish:
          task3_pub1: "task3_publish1"
          task3_result: <% task(task3).result.stdout %>
        on-success:
          - task4

      task4:
        join: all
        action: core.local
        input:
          cmd: "echo '$.param: <$.param %>, $.task1_pub1: <% $.task1_pub1 %>'"
        publish:
          task4_pub1: "task4_publish1"
          task4_result: <% task(task4).result.stdout %>
```

**追記: 上記の記述は間違っています。詳しくはコメント欄をご覧ください。**

`task1 -> [ task2, task3 ] -> task4` という流れでActionが実行されることを期待しています。
また、変数を登録したり、その結果を参照したりしています。

各設定を見てみます。

#### publish

変数に格納し、値を他のActionでも使えるようにします。
publishで格納した値は、他のActionでは`<% $.(publish名) %>`で取得することができます。
逆に言うと、ここに登録しないと他のActionから参照することができません。
`$`には様々な変数が格納されているので、一度全てを見てみてもいいでしょう。
例えば、最初に与えたinput (ここでは`param`)も、`$.param`として取得することができます。

#### 標準出力

`<% task(タスク名).result.stdout %>`という形で取得することができます。

#### on-success

Actionが成功したら、次のActionを指示します。

#### join:all

先行するActionが全て終わったらこのActionを動かします。

上記を作成したうえで、Workflowを登録します。

```bash
$ st2 action create /opt/stackstorm/packs/test/actions/mistral-advance.yaml
+-------------+--------------------------------+
| Property    | Value                          |
+-------------+--------------------------------+
| id          | 57be5ab5e368b916bff0d08d       |
| name        | mistral-advance                |
| pack        | test                           |
| description | Run a complex workflow         |
| enabled     | True                           |
| entry_point | workflows/mistral-advance.yaml |
| notify      |                                |
| parameters  | {                              |
|             |     "param": {                 |
|             |         "required": true,      |
|             |         "type": "string"       |
|             |     }                          |
|             | }                              |
| ref         | test.mistral-advance           |
| runner_type | mistral-v2                     |
| tags        |                                |
| uid         | action:test:mistral-advance    |
+-------------+--------------------------------+
```

それでは、実行してみます。

```bash
$ st2 run test.mistral-advance param=p
..................
id: 57be5b58e368b916bff0d08f
action.ref: test.mistral-advance
parameters:
  param: p
status: succeeded
start_timestamp: 2016-08-25T02:43:36.208702Z
end_timestamp: 2016-08-25T02:44:14.778810Z
+--------------------------+-------------------------+-------+------------+-------------------------------+
| id                       | status                  | task  | action     | start_timestamp               |
+--------------------------+-------------------------+-------+------------+-------------------------------+
| 57be5b5fe368b916bff0d092 | succeeded (8s elapsed)  | task1 | core.local | Thu, 25 Aug 2016 02:43:43 UTC |
| 57be5b6ae368b916bff0d095 | succeeded (8s elapsed)  | task2 | core.local | Thu, 25 Aug 2016 02:43:54 UTC |
| 57be5b6ae368b916bff0d096 | succeeded (13s elapsed) | task3 | core.local | Thu, 25 Aug 2016 02:43:54 UTC |
| 57be5b78e368b916bff0d098 | succeeded (4s elapsed)  | task4 | core.local | Thu, 25 Aug 2016 02:44:08 UTC |
+--------------------------+-------------------------+-------+------------+-------------------------------+
```

`start_timestamp`を見れば分かる通り、無事にtask2とtask3は並列で動いていることが分かります。

結果を詳細に見てみます。workflowのサブタスクまで含めた詳細な結果を見るためには、 `-d`オプションを付与します。
ただ、これだとテーブル形式での出力なので若干見づらいです…。実行オプションを見ると、`--attr`や`-k`などで絞り込めるみたいなのですが、なぜか全く意味を成しません。なんでだろう…？

```
$ st2 execution get 57be5b58e368b916bff0d08f -d
+-----------------+--------------------------------------------------------------+
| Property        | Value                                                        |
+-----------------+--------------------------------------------------------------+
| id              | 57be5b58e368b916bff0d08f                                     |
| action.ref      | test.mistral-advance                                         |
| context.user    | st2admin                                                     |
| parameters      | {                                                            |
|                 |     "param": "p"                                             |
|                 | }                                                            |
| status          | succeeded (38s elapsed)                                      |
| start_timestamp | Thu, 25 Aug 2016 02:43:36 UTC                                |
| end_timestamp   | Thu, 25 Aug 2016 02:44:14 UTC                                |
| result          | {                                                            |
|                 |     "tasks": [                                               |
|                 |         {                                                    |
|                 |             "state_info": null,                              |
|                 |             "name": "task1",                                 |
|                 |             "workflow_name": "test.mistral-advance.main",    |
|                 |             "created_at": "2016-08-25 02:43:42.250129",      |
|                 |             "updated_at": "2016-08-25 02:43:53.341116",      |
|                 |             "workflow_execution_id": "7e50bc87-b81c-481d-    |
|                 | 8c90-239bfe194570",                                          |
|                 |             "state": "SUCCESS",                              |
|                 |             "result": {                                      |
|                 |                 "failed": false,                             |
|                 |                 "stderr": "",                                |
|                 |                 "return_code": 0,                            |
|                 |                 "succeeded": true,                           |
|                 |                 "stdout": "$.param: p"                       |
|                 |             },                                               |
|                 |             "published": {                                   |
|                 |                 "task1_pub2": "task1_publish2",              |
|                 |                 "task1_pub1": "task1_publish1",              |
|                 |                 "task1_result": "$.param: p"                 |
|                 |             },                                               |
|                 |             "input": null,                                   |
|                 |             "id": "e8c54e34-10fc-49fc-bf12-3e4241f618c5"     |
|                 |         },                                                   |
|                 |         {                                                    |
|                 |             "state_info": null,                              |
|                 |             "name": "task2",                                 |
|                 |             "workflow_name": "test.mistral-advance.main",    |
|                 |             "created_at": "2016-08-25 02:43:53.335244",      |
|                 |             "updated_at": "2016-08-25 02:44:05.149236",      |
|                 |             "workflow_execution_id": "7e50bc87-b81c-481d-    |
|                 | 8c90-239bfe194570",                                          |
|                 |             "state": "SUCCESS",                              |
|                 |             "result": {                                      |
|                 |                 "failed": false,                             |
|                 |                 "stderr": "",                                |
|                 |                 "return_code": 0,                            |
|                 |                 "succeeded": true,                           |
|                 |                 "stdout": "$.param: <$.param %>,             |
|                 | $.task1_pub1: task1_publish1, $.task1_result: $.param: p"    |
|                 |             },                                               |
|                 |             "published": {                                   |
|                 |                 "task2_result": "$.param: <$.param %>,       |
|                 | $.task1_pub1: task1_publish1, $.task1_result: $.param: p",   |
|                 |                 "task2_pub1": "task2_publish2"               |
|                 |             },                                               |
|                 |             "input": null,                                   |
|                 |             "id": "ffd988de-591f-47cd-831d-8565ef725667"     |
|                 |         },                                                   |
|                 |         {                                                    |
|                 |             "state_info": null,                              |
|                 |             "name": "task3",                                 |
|                 |             "workflow_name": "test.mistral-advance.main",    |
|                 |             "created_at": "2016-08-25 02:43:53.430928",      |
|                 |             "updated_at": "2016-08-25 02:44:08.432897",      |
|                 |             "workflow_execution_id": "7e50bc87-b81c-481d-    |
|                 | 8c90-239bfe194570",                                          |
|                 |             "state": "SUCCESS",                              |
|                 |             "result": {                                      |
|                 |                 "failed": false,                             |
|                 |                 "stderr": "",                                |
|                 |                 "return_code": 0,                            |
|                 |                 "succeeded": true,                           |
|                 |                 "stdout": "$.param: <$.param %>,             |
|                 | $.task1_pub1: task1_publish1"                                |
|                 |             },                                               |
|                 |             "published": {                                   |
|                 |                 "task3_pub1": "task3_publish1",              |
|                 |                 "task3_result": "$.param: <$.param %>,       |
|                 | $.task1_pub1: task1_publish1"                                |
|                 |             },                                               |
|                 |             "input": null,                                   |
|                 |             "id": "1b671f81-d0f0-4504-a15d-584bb4ceea63"     |
|                 |         },                                                   |
|                 |         {                                                    |
|                 |             "state_info": null,                              |
|                 |             "name": "task4",                                 |
|                 |             "workflow_name": "test.mistral-advance.main",    |
|                 |             "created_at": "2016-08-25 02:44:05.130539",      |
|                 |             "updated_at": "2016-08-25 02:44:13.491646",      |
|                 |             "workflow_execution_id": "7e50bc87-b81c-481d-    |
|                 | 8c90-239bfe194570",                                          |
|                 |             "state": "SUCCESS",                              |
|                 |             "result": {                                      |
|                 |                 "failed": false,                             |
|                 |                 "stderr": "",                                |
|                 |                 "return_code": 0,                            |
|                 |                 "succeeded": true,                           |
|                 |                 "stdout": "$.param: <$.param %>,             |
|                 | $.task1_pub1: task1_publish1"                                |
|                 |             },                                               |
|                 |             "published": {                                   |
|                 |                 "task4_pub1": "task4_publish1",              |
|                 |                 "task4_result": "$.param: <$.param %>,       |
|                 | $.task1_pub1: task1_publish1"                                |
|                 |             },                                               |
|                 |             "input": null,                                   |
|                 |             "id": "0ff04b0c-2551-48d0-9a97-97064587b5de"     |
|                 |         }                                                    |
|                 |     ],                                                       |
|                 |     "extra": {                                               |
|                 |         "state_info": null,                                  |
|                 |         "state": "SUCCESS"                                   |
|                 |     },                                                       |
|                 |     "output2": "$.param: <$.param %>, $.task1_pub1:          |
|                 | task1_publish1, $.task1_result: $.param: p",                 |
|                 |     "output3": "$.param: <$.param %>, $.task1_pub1:          |
|                 | task1_publish1",                                             |
|                 |     "output1": "$.param: p",                                 |
|                 |     "output4": "$.param: <$.param %>, $.task1_pub1:          |
|                 | task1_publish1"                                              |
|                 | }                                                            |
| liveaction      | {                                                            |
|                 |     "runner_info": {                                         |
|                 |         "hostname": "demo1",                                 |
|                 |         "pid": 5339                                          |
|                 |     },                                                       |
|                 |     "parameters": {                                          |
|                 |         "param": "p"                                         |
|                 |     },                                                       |
|                 |     "action_is_workflow": true,                              |
|                 |     "callback": {},                                          |
|                 |     "action": "test.mistral-advance",                        |
|                 |     "id": "57be5b58e368b916bff0d08e"                         |
|                 | }                                                            |
+-----------------+--------------------------------------------------------------+
```

非常に見づらいので、resultの部分だけ。

```json:result
{
  "tasks": [
    {
      "state_info": null,
      "name": "task1",
      "created_at": "2016-08-25 02:43:42.250129",
      "updated_at": "2016-08-25 02:43:53.341116",
      "id": "e8c54e34-10fc-49fc-bf12-3e4241f618c5",
      "workflow_execution_id": "7e50bc87-b81c-481d-8c90-239bfe194570",
      "state": "SUCCESS",
      "result": {
        "succeeded": true,
        "failed": false,
        "return_code": 0,
        "stderr": "",
        "stdout": "$.param: p"
      },
      "published": {
        "task1_pub2": "task1_publish2",
        "task1_pub1": "task1_publish1",
        "task1_result": "$.param: p"
      },
      "input": null,
      "workflow_name": "test.mistral-advance.main"
    },
    {
      "state_info": null,
      "name": "task2",
      "created_at": "2016-08-25 02:43:53.335244",
      "updated_at": "2016-08-25 02:44:05.149236",
      "id": "ffd988de-591f-47cd-831d-8565ef725667",
      "workflow_execution_id": "7e50bc87-b81c-481d-8c90-239bfe194570",
      "state": "SUCCESS",
      "result": {
        "succeeded": true,
        "failed": false,
        "return_code": 0,
        "stderr": "",
        "stdout": "$.param: <$.param %>, $.task1_pub1: task1_publish1, $.task1_result: $.param: p"
      },
      "published": {
        "task2_result": "$.param: <$.param %>, $.task1_pub1: task1_publish1, $.task1_result: $.param: p",
        "task2_pub1": "task2_publish2"
      },
      "input": null,
      "workflow_name": "test.mistral-advance.main"
    },
    {
      "state_info": null,
      "name": "task3",
      "created_at": "2016-08-25 02:43:53.430928",
      "updated_at": "2016-08-25 02:44:08.432897",
      "id": "1b671f81-d0f0-4504-a15d-584bb4ceea63",
      "workflow_execution_id": "7e50bc87-b81c-481d-8c90-239bfe194570",
      "state": "SUCCESS",
      "result": {
        "succeeded": true,
        "failed": false,
        "return_code": 0,
        "stderr": "",
        "stdout": "$.param: <$.param %>, $.task1_pub1: task1_publish1"
      },
      "published": {
        "task3_pub1": "task3_publish1",
        "task3_result": "$.param: <$.param %>, $.task1_pub1: task1_publish1"
      },
      "input": null,
      "workflow_name": "test.mistral-advance.main"
    },
    {
      "state_info": null,
      "name": "task4",
      "created_at": "2016-08-25 02:44:05.130539",
      "updated_at": "2016-08-25 02:44:13.491646",
      "id": "0ff04b0c-2551-48d0-9a97-97064587b5de",
      "workflow_execution_id": "7e50bc87-b81c-481d-8c90-239bfe194570",
      "state": "SUCCESS",
      "result": {
        "succeeded": true,
        "failed": false,
        "return_code": 0,
        "stderr": "",
        "stdout": "$.param: <$.param %>, $.task1_pub1: task1_publish1"
      },
      "published": {
        "task4_pub1": "task4_publish1",
        "task4_result": "$.param: <$.param %>, $.task1_pub1: task1_publish1"
      },
      "input": null,
      "workflow_name": "test.mistral-advance.main"
    }
  ],
  "extra": {
    "state_info": null,
    "state": "SUCCESS"
  },
  "output2": "$.param: <$.param %>, $.task1_pub1: task1_publish1, $.task1_result: $.param: p",
  "output3": "$.param: <$.param %>, $.task1_pub1: task1_publish1",
  "output1": "$.param: p",
  "output4": "$.param: <$.param %>, $.task1_pub1: task1_publish1"
}
```

~~`output*`にはちゃんと値が格納されていることが分かります。が、`param`は最上位のActionでしか取れていないですね。
つまり、第2Actionへ渡すときは、必ずPublishする必要がありそうです。
一方、一度Publishすると、直接的に関係のないActionでも値が取れていることが分かります。~~
記述が間違っていたようです。詳しくはコメント欄をご覧ください。

## まとめ

YMLファイルでフローが定義できるのはかなり便利です。
一方、ファイルを分割して、共通のActionを使い回すにはどうしたらいいのかな？というのは、できるのかどうか分かりません。

Mistralはかなりいろいろな使い方ができるよう[^2]なので、他にも便利な使い方ができそうです。
慣れるまでは大変ですが、柔軟なWorkflowを作れそうです。

### JenkinsのPipelineと比べてどうなのか？

もともとのモチベーションがJenkinsと比べてどうなの？というところだったので、それと比較した感想ですが、当然ですが一長一短あります。

[^1]: https://docs.stackstorm.com/packs.html
[^2]: http://docs.openstack.org/developer/mistral/dsl/dsl_v2.html