LoginSignup
1
1

More than 3 years have passed since last update.

Dockerで始めるStackstorm再入門2/3(条件分岐させるWorkflowと定期実行させるRuleを書く)

Last updated at Posted at 2019-12-27

本記事はDockerで始めるStackstorm再入門1/3(環境構築からOrquestaで書いたWorkflowの結果をslackに通知する)の第2部です。

0. 目次と構成図

本記事では第1部で取り上げることができなかったStackstormの構成ファイルの書き方についてお話しできればと思います。

  • 1. 本記事でお話する構成図
  • 2. Actionとは
  • 3. Workflowとは
  • 4. Workflowで引数や結果に応じて分岐させる書き方(基本編)
  • 5. Workflowで引数や結果に応じて分岐させる書き方(shellscript編)
  • 6. サンプルWorkflow
  • 7. Ruleを使ってAction/Workflowを定期実行
  • 8. shellscriptのデバッグについて
  • 9. 次回予告

6. サンプルWorkflowでは、リモートリポジトリのステータスの確認とコンテナの再立ち上げをshellscriptをActionの実行関数としながら、workflowを紹介し、その前の2から5でActionやworkflowの前提知識をお伝えしています。
最後にRuleとshellscriptのデバッグについて少し書いています。

1. 本記事でお話する構成図

第1部から抜粋します。
Dockerで始めるStackstorm再入門1/3(環境構築からOrquestaで書いたWorkflowの結果をslackに通知する) 3. 構成図とバージョン情報

Screenshot from 2019-12-16 22-01-15.png
※1
Stackstormコンテナ内から同コンテナ外にあるアプリ(flask/nginx)コンテナに接続するために、
ホストのDockerデーモンのソケットファイル(var/run/docker.sock)をマウントさせています。

※2
Gtihubからリポジトリをcloneする際、ssh鍵やパスワードを使わずにおこなうべく、Personal access tokensを使いました。
トークンの取得方法は下記の記事をご参照ください。
私はrepoをスコープとしてトークンを取得しました。

参考
Creating a personal access token for the command line - GitHub Help

2. Actionとは

Stackstormがおこなう処理のことです。

Actionの3種類(筆者調べ)

  • core.localなどStackstormが提供するAction
  • サードパーティが提供するAction(Ansible plabookがst2上で扱えるものなど)
  • 独自に作ったAction

Actionの構成ファイル

  • 処理を定義するyamlファイル(メタファイル)
  • Actionの実行スクリプト(Action Runnner

Writing Custom Actions
An action is composed of two parts:

A YAML metadata file which describes the action, and its inputs.

A script file which implements the action logic

As noted above, an action script can be written in an >arbitrary programming language, as long as it follows these >conventions:

Script should exit with 0 status code on success and non-zero >on error (e.g. 1)

All log messages should be printed to standard error

参考: Action Runners — StackStorm 3.1.0 documentation

Actionの実行コマンド

root@$HOSTNAME:/# st2 run mydemo_pack.get_distro

st2_run_action.png
出所:近日参加予定のLT資料より。(完成次第、リンクを貼ります。)

処理を定義するActionのメタファイル

ここでは、私が作ったmydemo_packというPackのActionのメタファイルを一例として取り上げます。

サンプルのActionでやっていること

  • cd $working_dir && git fetch -p && git checkout -q $branch && git statusをしてリモートリポジトリの更新を確認すること
/opt/stackstorm/pack/mydemo_pack/actions/git_status.yaml
---
name: "git_status"
pack: "mydemo_pack"
description: "git status"
enabled: true
runner_type: "local-shell-script"  # 後述します
entry_point: "scripts/git_status.sh" # 本メタファイルからみた相対パスとなります。
parameters:
  working_dir:
    type: "string"
    required: true
    position: 0
  branch:
    type: "string"
    required: true
    position: 1
  expected:
    type: "string"
    required: true
    position: 2

公式ドキュメントで紹介されていたrunner_type

このように複数ありますが、だいたい使うのは文字の背景がグレーとなっているものかなと思います。

  • local-shell-cmd
  • local-shell-script
  • remote-shell-cmd
  • remote-shell-script
  • python-script
  • http-request
  • action-chain
  • mistral-v2
  • cloudslang
  • inquirer
  • winrm-cmd
  • winrm-ps-cmd
  • winrm-ps-script

参考: Actions — StackStorm 3.1.0 documentation

なお、orqeustaについては取り上げられていませんが、orqeustaを使う場合、workflowのメタファイルにrunner_type: "orquesta"と書くことでorquestaを扱うことが出来ました。

参考: 
st2/orquesta-streaming-demo.meta.yaml at master · StackStorm/st2

Actionの実行スクリプト(Action Runnner)

これは先ほど処理を定義するメタファイルで取り上げたActionのメタファイルのなかのentry_point: "scripts/git_status.sh"です。

/opt/stackstorm/packs/mydemo_pack/actions/scripts/git_status.sh

#!/bin/bash

# exit 0以外のリターンコードが返ることがあれば、そこで抜けるようにする
set -e

working_dir=$1
branch=$2
expected=$3

if [ -d "$working_dir" ]; then
  cd $working_dir
  sudo git fetch -p
  sudo git checkout -q $branch

  # ローカルリポジトリは最新であることを想定してgit status
  if [ "$expected" = "up_to_date" ]; then
    output=$(sudo git status | grep -E "(Your)\s+(branch)\s+(is)\s+(up-to-date)\s+(with)\s+('origin/$branch')" | awk '{print $6}' | grep -oP "$branch")
  # ローカルリポジトリは最新ではないこと(リモートリポジトリから更新を受け取る必要があること)を想定してgit status
  elif [ "$expected" = "not_up_to_date" ]; then
    output=$(sudo git status | grep -E "(Your)\s+(branch)\s+(is)\s+(behind)\s+('origin/$branch')" | awk '{print $5}' | grep -oP "$branch")
  else
    echo "None of the condition met"
  fi

  output=$(echo ${output:="unknown"})

  # git statusの結果が想定通りであるか
  if [ "$output" = "$branch" ]; then
    exit 0
  fi
fi

3. Workflowとは

  • ひとつひとつの処理(Action)を下の図のように繋げたもの Screenshot from 2019-12-21 16-18-16.png

workflowのメタファイル

  • workflowのメタファイルの記載項目は、Actionのメタファイルのそれとほとんど変わらない
  • 強いて言えばメタファイルを書くworkflowをst2 runで引数について定義することなく実行させる場合、default: "hoge"などと引数の値を定義する必要があるということと、runner_type: "orquesta"くらい
  • workflowはActionと違い、メタファイルの拡張子とworkflow自身のファイルの拡張子が同じyamlと紛らわしい

※私はWorkflowのメタファイルは*.meta.yamlと命名しています。

/opt/stackstorm/packs/mydemo_pack/actions/poll-repo.meta.yaml
---
name: "poll-repo"
pack: "mydemo_pack"
description: "poll repo"
runner_type: "orquesta"
entry_point: "workflows/poll-repo.yaml"
enabled: true
parameters:
  working_dir:
    type: "string"
    required: true
    default: "/usr/src/app/flask-docker"
  branch:
    type: "string"
    required: true
    default: "devel-views"
  expected:
    type: "string"
    required: true
    default: "up_to_date"
  ptn:
    type: "string"
    required: true
    default: "flask-docker_flask|flask-docker_nginx"
  timeout:
    type: "integer"
    required: true
    default: 300

4. Workflowで引数や結果に応じて分岐させる書き方(基本編)

以下の2つはAction問わず、使うと思います。

  • Actionの成否
  • 引数や変数の値に応じて

Actionの成否

  • Actionが成功したというのは、Actionのreturn codeが0であるとき、失敗したというのは、0以外であるとき(筆者調べ)
Actionが成功した場合.yaml
- when: <% succeeded() %>` 
Actionが失敗した場合.yaml
- when: <% failed() %>` 

引数や変数の値に応じて

boolean値で分岐させる場合.yaml

tasks:
  init:
    action: core.noop
    next:
      - do: doSth
        publish:
          - failed: False  # boolean値を初期化
 doSth:
    action: xxxx
    next:
      - when: <% failed() %> 
        do: last
        publish:
          - failed: True  # boolean値を更新

 last:
   action: core.noop
     next:
       - when: <% ctx().failed %> # boolean値がTrueであるときfailを実行
         do: fail
     #- when: <% not ctx().failed %> # boolean値がFalseであるとき
       #  do: yyyy
引数の値に応じて.抜粋.yaml

   # 「6. サンプルWorkflow」で取り上げているworkflowの抜粋です

  # expectedはworkflowのメタファイルでdefault値として定義することと併せて、Actionの成否に応じて値を更新(publish)しています

    next:
      - when: <% succeeded() and (ctx().expected = 'up_to_date') %>
        do: last
      - when: <% succeeded() and (ctx().expected = 'not_up_to_date') %>
        do: git_merge
        publish:
          - failed: False

5. Workflowで引数や結果に応じて分岐させる書き方(shellscript編)

shellscriptでActionを実行する場合、Actionの結果に応じてworkflowを条件分岐する際に使うことができる方法は以下の3つです。(筆者調べ)

  • Actionの成否(上述)
  • 引数や変数の値に応じて(上述)
  • リターンコードだけ
リターンコードで分岐.抜粋.yaml

 # 「6. サンプルWorkflow」で取り上げているworkflowの抜粋です

  rebuild_app: # 立ち上がっているコンテナを停止/削除した後、再立ち上げを図る
    action: mydemo_pack.rebuild_app
    input:
      working_dir: <% ctx().working_dir %>
      ptn: <% ctx().ptn %>
      timeout: <% ctx().timeout %>
    next:
      - when: <% succeeded() %>
        do: post_msg
        publish:
          - failed: False
          - action_result: <% result() %>
      - when: <% failed() %>
        do: check_failed
        publish:
          - action_result: <% result() %>
          - rc: <% result().return_code %>

  check_failed: # rebuild_appがfailedした原因を調査
    action: core.noop
    next:
    - when: <% ctx().rc = 201 %>  # failedの原因は停止/削除するコンテナがなかったことなので、改めてコンテナの再立ち上げを図る
      do: rebuild_app_cmd
    - when: <% ctx().rc != 201 %>
      do: post_msg
      publish:
      - failed: True 

リターンコードはshellscriptでこのように返却しています。

/opt/stackstorm/packs/mydemo_pack/actions/scripts/rebuild_app.sh
#!/bin/bash

# exit 0以外のリターンコードが返ることがあれば、そこで抜けるようにする
set -e

working_dir=$1
ptn=$2
counter=0

if [ -d "$working_dir" ]; then
  cd $working_dir
  # image idを取得
  ids=$(sudo docker container ls | grep -E "${ptn}" | awk '{print $1}')
 # image idをfor-loopでstop/rmしていき、成功すれば$counterをインクルメント
 # image idがひとつもなければfor-loopは行われず、$counterもインクルメントされない
  for i in $ids;
  do
    sudo docker container stop $i \
    && sudo docker container rm $i;
    counter=`expr $counter + 1`
  done
  # image idから2回特定のコンテナをstop/rmsしている場合
  if [ $counter -eq 2 ]; then
    sudo docker-compose up -d --build
    counter=`expr $counter + 1`
  # 1度も特定のコンテナをstop/rmsしていない場合(そもそもコンテナが立ち上がっていない場合)
  elif [ $counter -eq 0 ]; then
    exit 201
  fi
fi

if [ $counter -eq 3 ]; then
  exit 0
fi

6. サンプルのWorkflow

ここまでお話したworkflowの書き方について盛り込んだサンプルのWorkflowの/opt/stackstorm/packs/mydemo_pack/actions/workflows/poll-repo.yamlとそれを図式化したので、ご参照ください。
st2_run_wf_sh.png

  • 左上のpolling/$SECONDSが指しているgit statusがサンプルのWorkflowの実質的に最初に行われるAction(実際はfailed=Falseとフラグを初期化するcore.noopが最初に行うAction)
/opt/stackstorm/packs/mydemo_pack/actions/workflows/poll-repo.yaml
version: 1.0

description: poll remote repo

input:
  - working_dir
  - branch
  - expected
  - ptn
  - timeout

output:
  - failed: <% ctx().failed %>
  - action_name: <% ctx().action_name %>

tasks:
  init:  # failedフラグを初期化(False)とするだけのAction
    action: core.noop
    next:
    - publish:
        - failed: False
        - action_name: 'poll_repo'
      do: git_status_before_merged

  git_tatus_before_merged: # ローカルリポジトリは最新であることを想定してgit status
    action: mydemo_pack.git_status
    input:
      working_dir: <% ctx().working_dir %>
      branch: <% ctx().branch %>
      expected: <% ctx().expected %> #デフォルト値は'up_to_date'
      timeout: <% ctx().timeout %>
    next:
      - when: <% succeeded() and (ctx().expected = 'up_to_date') %>
        do: last
      - when: <% succeeded() and (ctx().expected = 'not_up_to_date') %> # 'not_up_to_date'(最新ではないこと)が確認できた
        do: git_merge
        publish:
          - failed: False
      - when: <% failed() and (not ctx().failed) %>
        do: git_status_before_merged
        publish:
          - failed: True
          - expected: 'not_up_to_date'
      - when: <% failed() and (ctx().failed) %>
        do: post_msg
        publish:
          - action_result: |-
              [result]
              <% result() %>

  git_merge:  # git_status_before_mergedで、ローカルリポジトリは最新ではないことが確認出来きた場合のみ実行
    action: core.local
    input:
      cmd: sudo git merge origin/<% ctx().branch %>
      cwd: <% ctx().working_dir %>
      timeout: <% ctx().timeout %>
    next:
      - when: <% succeeded() %>
        do: git_status_after_merged  
        publish:
          - expected: 'up_to_date'
      - when: <% failed() %>
        do: post_msg
        publish:
          - failed: True
          - action_result: |-
              [result]
              <% result() %>

  git_status_after_merged:     # ローカルリポジトリは最新であるとしてgit_status_after_mergedを実行
    action: mydemo_pack.git_status
    input:
      working_dir: <% ctx().working_dir %>
      branch: <% ctx().branch %>
      expected: <% ctx().expected %>
      timeout: <% ctx().timeout %>
    next:
      - when: <% succeeded() %>
        do: rebuild_app
        publish:
          - action_result: |-
              [result]
              <% result() %>
      - when: <% failed() %>
        do: post_msg
        publish:
          - failed: True
          - action_result: |-
              [result]
              <% result() %>

  rebuild_app: # 立ち上がっているコンテナを停止/削除した後、再立ち上げを図る
    action: mydemo_pack.rebuild_app
    input:
      working_dir: <% ctx().working_dir %>
      ptn: <% ctx().ptn %>
      timeout: <% ctx().timeout %>
    next:
      - when: <% succeeded() %>
        do: post_msg
        publish:
          - failed: False
          - action_result: <% result() %>
      - when: <% failed() %>
        do: check_failed
        publish:
          - action_result: <% result() %>
          - rc: <% result().return_code %>

  check_failed: # rebuild_appがfailedした原因を調査
    action: core.noop
    next:
    - when: <% ctx().rc = 201 %>  # failedの原因は停止/削除するコンテナがなかったことなので、改めてコンテナの再立ち上げを図る
      do: rebuild_app_cmd
    - when: <% ctx().rc != 201 %>
      do: post_msg
      publish:
      - failed: True 

  rebuild_app_cmd: # docker-compose up -d --buildを実行
    action: core.local
    input:
      cmd: sudo docker-compose up -d --build
      cwd: <% ctx().working_dir %>
      timeout: 600
    next:
      - when: <% succeeded() %>
        do: post_msg
        publish:
          - action_result: |-
              [result]
              <% result() %>
      - when: <% failed() %>
        do: post_msg
        publish:
          - action_result: |-
              [result]
              <% result() %>

  post_msg:  # workflownの成否をslackに通知
    action: slack.post_message
    input:
      message: |-
        [action_name] 
        <% ctx().action_name %>
        [failed]
        <% ctx().failed %>
        [action_result]
        <% ctx().action_result %>
    next:
      - do: last

  last:  # workflow全体の成否を決める上で考慮するべき失敗したActionがあるかfaildフラグで確認
    action: core.noop
    next:
      - when: <% ctx().failed %>
        do: fail # workflow全体の結果をfailedとするorquestaが提供するEngine Command

failとはなにか

  • failは、orquestaが提供する、workflow全体の結果をfailedとするorquestaが提供するEngine Command
  • そもそもworkflowのステータスは最後のActionのステータスによって決まるので、途中のActionの失敗をworkflowのステータスに反映出来ない
  • そこで使い方として、以下の使い方が考えられる
    • フラグを冒頭で定義して、
    • 任意のActionの成否に応じてフラグの値を更新し、
    • 最後のActionでフラグの値に応じてfailを実行する

The workflow engine will fail the workflow execution.

参考: Orquesta Workflow Definition — StackStorm 3.1.0 documentation

シェルスクリプトでActionを走らせる場合の課題

  • シェルスクリプトで走らせているActionの成否判定をWorkflowに適用させるにはreturn codeを使うしかない
  • Stackstormではreturn codeが0のとき、そのActionのstatusは成功(succeeded)、0以外のときは失敗(fialed)となっている
  • そのため、シェルスクリプトを使ったActionでWorkflowを条件分岐させには、WorkflowのなかのActionのstatusがfailedとなることを許容するしかなく、結果としてWorkflow全体として成功したのか失敗したのか分かりづらくなってしまう

ローカルリポジトリが最新であるときにworkflowが実行された場合

(Actionは全てSucceededとなり、見通しがよい)

root@$HOSTNAME:/# st2 run mydemo_pack.poll-repo
..
id: 5e0ac43ca38413013c720b10
action.ref: mydemo_pack.poll-repo
parameters: None
status: succeeded
start_timestamp: Tue, 31 Dec 2019 03:45:00 UTC
end_timestamp: Tue, 31 Dec 2019 03:45:03 UTC
result: 
  output:
    action_name: poll_repo
    failed: false
+--------------------------+------------------------+--------------------------+------------------------+-------------------------------+
| id                       | status                 | task                     | action                 | start_timestamp               |
+--------------------------+------------------------+--------------------------+------------------------+-------------------------------+
| 5e0ac43da38413003a9c56e8 | succeeded (0s elapsed) | init                     | core.noop              | Tue, 31 Dec 2019 03:45:01 UTC |
| 5e0ac43da38413003a9c56eb | succeeded (1s elapsed) | git_status_before_merged | mydemo_pack.git_status | Tue, 31 Dec 2019 03:45:01 UTC |
| 5e0ac43fa38413003a9c56ee | succeeded (0s elapsed) | last                     | core.noop              | Tue, 31 Dec 2019 03:45:03 UTC |
+--------------------------+------------------------+--------------------------+------------------------+-------------------------------+
root@$HOSTNAME:/# st2 execution get 5e0ac43da38413003a9c56eb
id: 5e0ac43da38413003a9c56eb
status: succeeded (1s elapsed)
parameters: 
  branch: devel-views
  expected: up_to_date
  timeout: 300
  working_dir: /usr/src/app/flask-docker
result: 
  failed: false
  return_code: 0
  stderr: ''
  stdout: ''
  succeeded: true
root@$HOSTNAME:/# 

ローカルリポジトリが最新ではないときにworkflowが実行された場合

(ローカルリポジトリが最新であることを想定して実行されたAction(上から2個目のgit_status_before_merged)を除いてSucceededとなり、見通しは悪い。Workflow全体として成功しているのか、st2 execution get $IDで中身を精査すっる必要があるのか分かりづらい。)

root@$HOSTNAME:/# st2 run mydemo_pack.poll-repo
........
id: 5e0acdefa38413013c720b16
action.ref: mydemo_pack.poll-repo
parameters: None
status: succeeded
start_timestamp: Tue, 31 Dec 2019 04:26:23 UTC
end_timestamp: Tue, 31 Dec 2019 04:26:38 UTC
result: 
  output:
    action_name: poll_repo
    failed: false
+--------------------------+------------------------+--------------------------+-------------------------+-------------------------------+
| id                       | status                 | task                     | action                  | start_timestamp               |
+--------------------------+------------------------+--------------------------+-------------------------+-------------------------------+
| 5e0acdf0a38413003a9c5709 | succeeded (0s elapsed) | init                     | core.noop               | Tue, 31 Dec 2019 04:26:24 UTC |
| 5e0acdf0a38413003a9c570c | failed (2s elapsed)    | git_status_before_merged | mydemo_pack.git_status  | Tue, 31 Dec 2019 04:26:24 UTC |
| 5e0acdf2a38413003a9c570f | succeeded (1s elapsed) | git_status_before_merged | mydemo_pack.git_status  | Tue, 31 Dec 2019 04:26:26 UTC |
| 5e0acdf4a38413003a9c5712 | succeeded (0s elapsed) | git_merge                | core.local              | Tue, 31 Dec 2019 04:26:28 UTC |
| 5e0acdf4a38413003a9c5715 | succeeded (1s elapsed) | git_status_after_merged  | mydemo_pack.git_status  | Tue, 31 Dec 2019 04:26:28 UTC |
| 5e0acdf6a38413003a9c5718 | succeeded (5s elapsed) | rebuild_app              | mydemo_pack.rebuild_app | Tue, 31 Dec 2019 04:26:30 UTC |
| 5e0acdfca38413003a9c571b | succeeded (1s elapsed) | post_msg                 | slack.post_message      | Tue, 31 Dec 2019 04:26:36 UTC |
| 5e0acdfda38413003a9c571e | succeeded (1s elapsed) | last                     | core.noop               | Tue, 31 Dec 2019 04:26:37 UTC |
+--------------------------+------------------------+--------------------------+-------------------------+-------------------------------+
root@$HOSTNAME:/# st2 execution get 5e0acdf0a38413003a9c570c
id: 5e0acdf0a38413003a9c570c
status: failed (2s elapsed)
parameters: 
  branch: devel-views
  expected: up_to_date
  timeout: 300
  working_dir: /usr/src/app/flask-docker
result: 
  failed: true
  return_code: 1
  stderr: "From https://github.com/gkzz/flask-docker
   83ce3b6..027a009  devel-views -> origin/devel-views"
  stdout: ''
  succeeded: false
root@$HOSTNAME:/# st2 execution get 5e0acdf2a38413003a9c570f
id: 5e0acdf2a38413003a9c570f
status: succeeded (1s elapsed)
parameters: 
  branch: devel-views
  expected: not_up_to_date
  timeout: 300
  working_dir: /usr/src/app/flask-docker
result: 
  failed: false
  return_code: 0
  stderr: ''
  stdout: ''
  succeeded: true
root@$HOSTNAME:/# 

7. Ruleを使ってAction/Workflowを定期実行

以下のyamlを作ってください。

root@$HOSTNAME:/# cat /opt/stackstorm/packs/mydemo_pack/rules/timer.yaml
---
name: "timer"
pack: "mydemo_pack"
description: "run mydemo per 300 secs"
enabled: true
trigger:
  type: "core.st2.IntervalTimer"
  parameters:
    unit: seconds
    #delta: 30
    delta: 300
action:
  ref: "mydemo_pack.poll-repo"
  parameters:
    working_dir: "/usr/src/app/flask-docker"
    branch: "devel-views"
    expected: "up_to_date"
    ptn: "flask-docker_flask|flask-docker_nginx"

それではRuleを作るコマンドを以下のとおり実行すれば設定完了です。

root@$HOSTNAME:/# st2 rule create timer.yaml

8. shellscriptのデバッグについて

以下の2つの方法でデバッグさせていました。

  • Action単体で実行
root@$HOSTNAME:/# st2 run mydmeo_pack.git_status
  • bash -x /path/to/$filenameでデバッグ
root@$HOSTNAME:/# bash -x /opt/stackstorm/packs/mydemo_pack/actions/scripts/git_status.sh \
> /usr/src/app/flask-docker \
> flask-docker_flask|flask-docker_nginx

9. 次回予告

shellscriptでリモートリポジトリのステータスの確認とコンテナの再立ち上げを行いましたが、今度はそれをpythonでおこなってみます。
pythonでActionの実行スクリプトを書くと、shellscriptに比べてActionの返却値(return value)を柔軟に定義することができたり、mockを使ってテストコードも書くことができるので、よりきめ細かくActionを書くことができます。

お楽しみに!!

鋭意作成中
Dockerで始めるStackstorm再入門3/3(pythonスクリプトとmockを使ったテストコードを書く)

参考

P.S. Twitterもやってるのでフォローしていただけると泣いて喜びます:)

@gkzvoice

P.P.S. StackstormでLTしました!

StackstormというIFTTT的なツールをDockerコンテナに載せた小話

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1