本記事はDockerで始めるStackstorm再入門1/3(環境構築からOrquestaで書いたWorkflowの結果をslackに通知する)の第2部です。
- Dockerで始めるStackstorm再入門1/3(環境構築からOrquestaで書いたWorkflowの結果をslackに通知する)
- Dockerで始めるStackstorm再入門2/3(条件分岐させるWorkflowと定期実行させるRuleを書く)
- Dockerで始めるStackstorm再入門3/3(pythonスクリプトとmockを使ったテストコードを書く)
- 番外編
0. 目次と構成図
本記事では第1部で取り上げることができなかったStackstormの構成ファイルの書き方についてお話しできればと思います。
-
- 本記事でお話する構成図
-
- Actionとは
-
- Workflowとは
-
- Workflowで引数や結果に応じて分岐させる書き方(基本編)
-
- Workflowで引数や結果に応じて分岐させる書き方(shellscript編)
-
- サンプルWorkflow
-
- Ruleを使ってAction/Workflowを定期実行
-
- shellscriptのデバッグについて
-
- 次回予告
6. サンプルWorkflow
では、リモートリポジトリのステータスの確認とコンテナの再立ち上げをshellscriptをActionの実行関数としながら、workflowを紹介し、その前の2から5でActionやworkflowの前提知識をお伝えしています。
最後にRuleとshellscriptのデバッグについて少し書いています。
1. 本記事でお話する構成図
第1部から抜粋します。
Dockerで始めるStackstorm再入門1/3(環境構築からOrquestaで書いたWorkflowの結果をslackに通知する) 3. 構成図とバージョン情報
※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
出所:近日参加予定のLT資料より。(完成次第、リンクを貼ります。)
処理を定義するActionのメタファイル
ここでは、私が作ったmydemo_pack
というPackのActionのメタファイルを一例として取り上げます。
サンプルのActionでやっていること
-
cd $working_dir && git fetch -p && git checkout -q $branch && git status
をしてリモートリポジトリの更新を確認すること
---
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"
です。
#!/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とは
workflowのメタファイル
- workflowのメタファイルの記載項目は、Actionのメタファイルのそれとほとんど変わらない
- 強いて言えばメタファイルを書くworkflowをst2 runで引数について定義することなく実行させる場合、
default: "hoge"
などと引数の値を定義する必要があるということと、runner_type: "orquesta"
くらい - workflowはActionと違い、メタファイルの拡張子とworkflow自身のファイルの拡張子が同じyamlと紛らわしい
※私はWorkflowのメタファイルは*.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以外であるとき(筆者調べ)
- when: <% succeeded() %>`
- when: <% failed() %>`
引数や変数の値に応じて
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
# 「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の成否(上述)
- 引数や変数の値に応じて(上述)
- リターンコードだけ
# 「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でこのように返却しています。
#!/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
とそれを図式化したので、ご参照ください。
- 左上の
polling/$SECONDS
が指しているgit status
がサンプルのWorkflowの実質的に最初に行われるAction(実際はfailed=False
とフラグを初期化するcore.noopが最初に行うAction)
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を使ったテストコードを書く)
参考
-
[Ubuntu Linux を実行しているプライベート Amazon EC2 インスタンスに静的ホスト名を割り当てる]
(https://aws.amazon.com/jp/premiumsupport/knowledge-center/linux-static-hostname) -
[シェルスクリプトのreturn - 考える人、コードを書く人]
(https://bokko.hatenablog.com/entry/20090223/1235400626) -
orqeustaのサンプルコード
-
[github.com/StackStorm/st2/blob/master/contrib/examples/actions/workflows/orquesta-streaming-demo.yaml]
(https://github.com/StackStorm/st2/blob/master/contrib/examples/actions/workflows/orquesta-streaming-demo.yaml)
-
-
本記事で扱ったStackstormのサンプルコード
-
Stackstormの管理対象のアプリコンテナ
-
ActionとWorkflowのサンプルコード