🌪️

イベント駆動ワークフローエンジン stackstorm を試す

2023/11/23に公開

概要

stackstorm (以下 st2) は python 製の OSS のイベント駆動型ワークフローツールです。
Linux Foundation project の一部であり、trusted by the enterprise には Cisco, NASA, Netflix などの有名企業が名を連ねています。Github star 数は現時点で 5.8 k となっており利用者も多いことがわかります。

https://stackstorm.com/

イベント駆動型というのは、例えば github に PR が建てられた等や監視対象のサーバがダウンした等のイベントを起点として処理を開始するような仕組みのことを指します。同様の web サービスでは IFTTT が有名であり、st2 もよく比較されています。

ワークフローという面に注目すると今までの記事で紹介した concoursetekton などと同じカテゴリに位置づけられます。ただ concourse や tekton がクラウドネイティブな CI/CD を実現するツールであるのに対し、stackstorm は複数サービスの連携や運用、対応の自動化といった面に焦点を当てています (もちろん使いようによっては CI/CD に組み込むことも可能)。

インストール

インストール方法は ドキュメント にまとまっています。st2 自体はクラウドネイティブなプロジェクトではないためマシンに直接インストールする方法が多いですが、helm を使って k8s クラスタにデプロイする 方法があるのでこちらで k8s クラスタへインストールします。

まずは helm repository を登録して values.yml を出力します。

helm repo add stackstorm https://helm.stackstorm.com/
helm show values st2/stackstorm-ha > values.yml

k8s でのインストールでは、後述する pack を共有するための方法として Custom st2 packs に 2 種類の方法が記載されています。今回は k8s persistentVolumeClaim を使用して runner 間で pack を共有する方法を使うため、values.yml の中身を以下のように書き換えます。

values.yml
-       enabled: false
+       enabled: true

-       packs: {}
+       packs:
+         persistentVolumeClaim:
+           claimName: st2-pack

<       virtualenvs: {}
+       virtualenvs:
+         persistentVolumeClaim:
+           claimName: st2-virtualenv

-       configs: {}
+       configs:
+         persistentVolumeClaim:
+           claimName: st2-config

ここで使用する persistentVolumeClaim リソースを事前に作成しておきます。
storage size はユースケースにより異なりますが、ここでは適当にそれぞれ 5 GB を割り当てます。

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: st2-pack
  namespace: st2
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 5Gi

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: st2-config
  namespace: st2
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 5Gi

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: st2-virtualenv
  namespace: st2
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 5Gi

また、これに対応する persistentVolume も作成する必要があります。
手動で作成しても良いですが、今回は事前に k8s クラスタに openEBS をデプロイしておき、pvc が pod に割り当てられた際に動的に pv を作成するようにします。

https://openebs.io/

準備ができたらデプロイします。

helm install st2 -n st2 st2/stackstorm-ha -f values.yml

デプロイが完了すると st2 コンポーネントに対応するかなりの数の pod が作成されます。

$ kubectl get pod
NAME                                       READY   STATUS      RESTARTS      AGE
st2-job-st2-apikey-load-mtxkn              0/1     Completed   0             42h
st2-job-st2-key-load-q7p6x                 0/1     Completed   0             42h
st2-job-st2-register-content-nqg4v         0/1     Completed   0             42h
st2-mongodb-0                              1/1     Running     0             42h
st2-mongodb-1                              1/1     Running     0             42h
st2-mongodb-2                              1/1     Running     0             42h
st2-rabbitmq-0                             1/1     Running     0             42h
st2-rabbitmq-1                             1/1     Running     0             42h
st2-rabbitmq-2                             1/1     Running     0             42h
st2-redis-node-0                           2/2     Running     0             42h
st2-redis-node-1                           2/2     Running     0             42h
st2-redis-node-2                           2/2     Running     0             42h
st2-st2actionrunner-d4988dffc-466dq        1/1     Running     0             179m
st2-st2actionrunner-d4988dffc-9dj9m        1/1     Running     0             179m
st2-st2actionrunner-d4988dffc-dnxwj        1/1     Running     0             179m
st2-st2actionrunner-d4988dffc-kdgw9        1/1     Running     0             179m
st2-st2actionrunner-d4988dffc-t6j9q        1/1     Running     0             179m
st2-st2api-6c5874d95c-hrw5n                1/1     Running     0             42h
st2-st2api-6c5874d95c-vf25v                1/1     Running     0             42h
st2-st2auth-774dfddc4c-cc2ln               1/1     Running     0             42h
st2-st2auth-774dfddc4c-v57xc               1/1     Running     0             42h
st2-st2client-5fb9c79dcd-snlfr             1/1     Running     0             42h
st2-st2garbagecollector-6c64ff5bc9-x8gvd   1/1     Running     0             42h
st2-st2notifier-55d5df6976-dsdq7           1/1     Running     0             42h
st2-st2notifier-55d5df6976-tflts           1/1     Running     0             42h
st2-st2rulesengine-54dcc8dd9f-4td89        1/1     Running     0             42h
st2-st2rulesengine-54dcc8dd9f-xfw7h        1/1     Running     0             42h
st2-st2scheduler-597c9b488f-j7mxc          1/1     Running     4 (42h ago)   42h
st2-st2scheduler-597c9b488f-ll7xn          1/1     Running     4 (42h ago)   42h
st2-st2sensorcontainer-6949b97fb6-rmxzb    1/1     Running     0             42h
st2-st2stream-7b9878447-42ztf              1/1     Running     0             42h
st2-st2stream-7b9878447-vqqb5              1/1     Running     0             42h
st2-st2timersengine-844999d648-xllrb       1/1     Running     0             42h
st2-st2web-7b695b6d59-ftwz4                1/1     Running     0             42h
st2-st2web-7b695b6d59-vprsb                1/1     Running     0             42h
st2-st2workflowengine-86bbb589f4-7xgtl     1/1     Running     0             42h
st2-st2workflowengine-86bbb589f4-fmgfn     1/1     Running     0             42h

svc

$ kubectl get svc
NAME                    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                 AGE
st2-mongodb-headless    ClusterIP   None             <none>        27017/TCP                               42h
st2-rabbitmq            ClusterIP   10.105.250.253   <none>        5672/TCP,4369/TCP,25672/TCP,15672/TCP   42h
st2-rabbitmq-headless   ClusterIP   None             <none>        4369/TCP,5672/TCP,25672/TCP,15672/TCP   42h
st2-redis               ClusterIP   10.108.249.198   <none>        6379/TCP,26379/TCP                      42h
st2-redis-headless      ClusterIP   None             <none>        6379/TCP,26379/TCP                      42h
st2-st2api              ClusterIP   10.103.84.133    <none>        9101/TCP                                42h
st2-st2auth             ClusterIP   10.109.68.142    <none>        9100/TCP                                42h
st2-st2stream           ClusterIP   10.105.237.199   <none>        9102/TCP                                42h
st2-st2web              NodePort    10.102.51.79     <none>        80:30341/TCP                            42h

svc の st2web が web UI に対応しており、username は st2admin, 以下のコマンドで取得した password でログインできます。

kubectl get secrets -o yaml st2-st2-auth | yq -r ".data.ST2_AUTH_PASSWORD" | base64 -d

使ってみる

実際にワークフローを作って動作を確認します。

st2 における action, workflow, pack の概念

ワークフローを作る前に st2 における action や workflow リソースの概念について簡単に説明しておきます。

Action

st2 では action と呼ばれるリソースが処理の最小単位になります。tekton や concourse における task オブジェクトに相当します。
action は 2 つのファイルから構成されます。

  • metadata: action の定義や入力パラメータを記載する yaml ファイル
  • script: 実際の処理を記述するファイル

script は Available Runner に使用可能な形式が記載されています。シェルスクリプトや python スクリプトの他に実行可能な linux コマンドも対応しているので、executable なバイナリが生成できるプログラム言語でも記述できそうです。
また、Ansible のようにリモートマシン上でスクリプトを実行する形式も指定可能となっています。

Workflow

workflow は複数の action を組み合わせて一連のワークフローとして実行するリソースであり、st2 内では Orquesta という用語で定義されています。
こちらも action と同様に 2 つのファイルから構成されます。

  • metadata: workflow の定義や入力パラメータを記載する yaml ファイル
  • workflow: workflow 内の処理記述する yaml ファイル

st2 における処理の実行単位は action であるため、ワークフロー処理では複数の action を組み合わせて好みの動作を実現します。その他、action 間での変数の受け渡しや action の実行結果に応じて後続の action を実行する条件分岐などを記述できます。

pack

st2 では複数の action や workflow をまとめて pack という単位で管理します。
例えばデフォルトでインストールされている linux pack では cp, rsync, service などの基本的な linux コマンドに対応する action や workflow が格納されています。

action や workflow はいずれかの pack 配下に登録する必要があります。自作の action 等を開発する場合はまず自作の pack を定義し、その中に pack の趣旨に沿った action, workflow を作成して登録するという流れになります。

ワークフローの作成

st2 での処理単位について簡単に紹介したところで、実際にワークフローを作成してみます。
今回は、今までのワークフロー処理でおなじみの git repository から dockerfile を含むコードを clone し、コンテナイメージを build, push, ビルド結果を slack に投稿するような一連のワークフロー処理を実現してみたいと思います。

st2 でこの動作を実現する場合、以下のような workflow と action を作成する必要があります。

  • action
    • git clone する処理
    • イメージを build, push する処理
    • slack に webhook でメッセージを投稿する処理
  • workflow
    • 上記を一連で実行するワークフロー

それぞれの作り方について以下で見ていきます。

action の作成

はじめに Action metadata を参考に、git clone を行う action の metadata を作成します。

git-clone.yml
description: 'Git clone action'
enabled: true
entry_point: git-clone.sh
name: git-clone
parameters:
  repository_url:
    type: string
    description: "Repository url"
    required: true
    position: 0
  branch:
    type: string
    description: "Branch"
    default: main
    position: 1
  username:
    type: string
    description: "Username"
    position: 2
  password:
    type: string
    description: "Password"
    position: 3
  tls_verify:
    type: boolean
    description: "Check if TLS verify"
    default: true
    position: 4
runner_type: "local-shell-script"

この action が呼ばれた際に実行されるシェルスクリプトを記述します。
オプションはいろいろ省いて単に入力パラメータで指定された repository から git clone するだけのシンプルな構成にします。

git-clone.sh
#!/bin/bash
set -ue

repository_url=$1
branch=$2
username=$3
password=$4
tls_verify=$5

repository_url="https://${username}:${password}@${repository_url#https://}"
last_element=$(basename "$repository_url")
destination=$(echo "$last_element" | sed 's/\.git$//')

if [[ ${tls_verify} == "false" ]]; then
    git config --global http.sslVerify false
fi

# Remove the previous directory
share_dir=/opt/stackstorm/virtualenvs/tmp
rm -rf ${share_dir}/${destination}

# Run git clone
git clone \
    ${repository_url} \
    -b ${branch} \
    ${share_dir}/${destination}

なお、action は基本的に別の st2actionrunner pod で実行されるため、デフォルトでは action 間でのファイルやディレクトリの共有ができません。
今回は pvc で共有している /opt/stackstorm/virtualenvs/ 以下に clone したディレクトリを保存することで後続の action で使用可能にしています。
ちゃんと作るのであれば共有用の pvc を別に用意し、runner pod 内でマウントするようにマニフェストを書き換えるのが良いでしょう。

次にディレクトリの Dockerfile からコンテナイメージを build, push する action を作ります。
ただし、諸々の事情からデフォルトでインストールされる st2actionrunner ではコンテナイメージの build が実行できないため、独自でビルドしたイメージを使用します。詳細は buildah が実行可能な st2actionrunner イメージの作成 を参照。
これに合わせて buildah を使ってイメージを build する action を作ります。

build.yml
description: 'Build and push an image action'
enabled: true
entry_point: build.sh
name: build
parameters:
  image_name:
    type: string
    description: "Image name"
    required: true
    position: 0
  image_tag:
    type: string
    description: "Image tag"
    default: latest
    position: 1
  directory:
    type: string
    description: "Path to the directory where Dockerfile exists"
    required: true
    position: 2
  dockerfile_name:
    type: string
    description: "Dockerfile filename"
    default: Dockerfile
    position: 3
  registry_host:
    type: string
    description: "Registry host [host]:[port]"
    required: true
    position: 4
  tls_verify:
    type: string
    description: "Check if TLS verify"
    default: "true"
    position: 5
runner_type: "local-shell-script"

buildah を実行するシェルスクリプト

build.sh
#!/bin/bash
set -ue

IMAGE_NAME=$1
IMAGE_TAG=$2
DIRECTORY=$3
DOCKERFILE_NAME=$4
REGISTRY_HOST=$5
TLS_VERIFY=$6


buildah build \
    -t ${IMAGE_NAME}:${IMAGE_TAG} \
    -f ${DIRECTORY}/${DOCKERFILE_NAME}

buildah tag ${IMAGE_NAME}:${IMAGE_TAG} ${REGISTRY_HOST}/${IMAGE_NAME}:${IMAGE_TAG}

buildah push \
    --tls-verify=${TLS_VERIFY} \
    ${REGISTRY_HOST}/${IMAGE_NAME}:${IMAGE_TAG}

最後に build 結果を slack に post する action を準備します。
これに関しては、いろいろな pack が事前に公開されている stackstorm exchange にある slack pack が利用できます。

pack をインストールするには st2client pod 上で st2 pack install slack を実行します。

kubectl exec -it  st2-st2client-5fb9c79dcd-snlfr -- st2 pack install slack

これにより slack pack に含まれる様々な action が使用可能になります。webhook で slack channel にメッセージを post するにはこの中の post_message action が使えます。

$ st2 action list -p slack
...
| slack.pins.add                          | slack | Pins an item to a channel.              |
| slack.pins.list                         | slack | Lists items pinned to a channel.        |
| slack.pins.remove                       | slack | Un-pins an item from a channel.         |
| slack.post_message                      | slack | Post a message to the Slack channel.    |
...

workflow の作成

次に、上記で作成した action を一連で実行するワークフローを作成します。

まず action と同様に workflow のメタデータを作成します。

build-workflow.yml
---
name: build-workflow
description: Clone git repository, build an image and push it to a remote registry.
runner_type: orquesta
entry_point: workflows/build-workflow.yml
enabled: true
parameters:
  repository_url:
    type: string
    description: "Git repository url such as https://..../repo.git"
    required: true
  branch:
    type: string
    description: "Gir branch"
    default: main
  username:
    type: string
    description: "Git username"
  password:
    type: string
    description: "Git password"
  image_name:
    type: string
    description: "Image name"
    required: true
  image_tag:
    type: string
    description: "Image tag"
    default: latest
  dockerfile_name:
    type: string
    description: "Dockerfile filename"
    default: Dockerfile
  registry_host:
    type: string
    description: "Registry host where the image to be pushed. [host]:[port]"
    required: true
  tls_verify:
    type: string
    description: "TLS Check"
    default: "true"

メタデータでは runner_type: orquesta を指定することで内部的に workflow と認識されます。

次に実際に実行されるワークフローを作成します。
ワークフローファイルではトップレベルの tasks で実行される action を定義します。例えば、git-clone action を実行するための記述は以下のようになります。

tasks:
  git_clone:
    action: cicd.git-clone
    input:
      repository_url: "{{ ctx().repository_url }}"
      branch: "{{ ctx().branch }}"
      username: "{{ ctx().username }}"
      password: "{{ ctx().password }}"
      tls_verify: "{{ ctx().tls_verify }}"
    next:
      - when: "{{ succeeded() }}"
        publish:
          - directory: "/opt/stackstorm/virtualenvs/tmp/{{ ctx().repository_url.split('/') | last | replace('.git', '') }}"
        do: build_push
      - when: "{{ failed() }}"
        publish:
          - build_status: Fail
          - result_message: "{{ result().stderr }}"
        do: post_result

action には [pack_name].[action_name] 形式で実行する action を指定します。
input 以下には action に受け渡すパラメータを key-value 形式で指定できます。
ワークフロー全体で input の変数や task の実行結果は ctx() という変数に格納され、例えば以下のいずれかの形式で変数 a の値を取得できます。

jinja 形式: "{{ ctx().a }}"
yaql 形式: <% ctx().a %>

詳細については Workflow Runtime Context を参照。
どちらの形式でも ok ですが、1 つのワークフロー内で両者の形式を混ぜて使用することはできません。
上記の input ではワークフロー実行時に指定したパラメータを取得し、git-clone アクションの入力パラメータに指定しています。

next ではこの action が終了した際の次の動作を list 形式で指定します。参考

Key Description
when 適用条件
publish 以降の task で使用する変数と値の指定。指定した変数は ctx() に格納される
do when に指定した条件を満たす際に次に実行される task 名

when では指定した式が true となる時に設定が適用されます。タスクが成功した際は "{{ succeeded() }}", 失敗した際は "{{ failed() }}" がそれぞれ true となるので、上記の例では以下の動作となります。

  • action が成功 (シェルスクリプトの終了コードが 0) した場合
    • build_push タスクを実行する。
  • action が失敗 (シェルスクリプトの終了コードが 0 以外) した場合
    • post_result タスクを実行する。
    • result_message 変数に stderr に出力されたメッセージを設定する。

後は同じようにコンテナイメージのビルド、slack への結果のポストを行うタスクを定義します。

  build_push:
    action: cicd.build
    input:
      image_name: "{{ ctx().image_name }}"
      image_tag: "{{ ctx().image_tag }}"
      directory: "{{ ctx().directory }}"
      dockerfile_name: "{{ ctx().dockerfile_name }}"
      registry_host: "{{ ctx().registry_host }}"
      tls_verify: "{{ ctx().tls_verify }}"
    next:
      - when: "{{ succeeded() }}"
        publish:
          - build_status: Success
        do: post_result
      - when: "{{ failed() }}"
        publish:
          - build_status: Fail
          - result_message: "{{ result().stderr }}"
        do: post_result
  post_result:
    action: slack.post_message
    input:
      message: |
        Build_statue: {{ ctx().build_status }}
        Repository: {{ ctx().repository_url }}
        Image: {{ ctx().registry_host }}/{{ ctx().image_name }}:{{ ctx().image_tag }}
        {% if ctx().result_message is not none %}
        message:
        ```
        {{ ctx().result_message }}
        ```
        {% endif %}
      channel: "#general"
      username: bot_test
      webhook_url: "https://hooks.slack.com/xxxxxxxxxxxxxxxxxxxx"

なお、ワークフロー全体で python のテンプレートエンジン jinja の表式が使用できます。
上記の post_message のように文字列の抽出、置換や、if 文を使って特定の条件を満たす場合に文字列を追加するなどの柔軟な表現ができます。

その他、Getting started の例に沿ってワークフローの定義に必要なフィールドなどを追加すると、最終的なワークフローの中身は以下のようになります。

workflows/build-workflow.yml
version: 1.0
description: Build image and push.
input:
  - repository_url
  - branch
  - username
  - password
  - image_name
  - image_tag
  - directory
  - dockerfile_name
  - registry_host
  - tls_verify
vars:
  - build_status: null
  - result_message: null
output:
  - result: null
tasks:
  git_clone:
    action: cicd.git-clone
    input:
      repository_url: "{{ ctx().repository_url }}"
      branch: "{{ ctx().branch }}"
      username: "{{ ctx().username }}"
      password: "{{ ctx().password }}"
      tls_verify: "{{ ctx().tls_verify }}"
    next:
      - when: "{{ succeeded() }}"
        publish:
          - directory: "/opt/stackstorm/virtualenvs/tmp/{{ ctx().repository_url.split('/') | last | replace('.git', '') }}"
        do: build_push
      - when: "{{ failed() }}"
        publish:
          - build_status: Fail
          - result_message: "{{ result().stderr }}"
        do: post_result
  build_push:
    action: cicd.build
    input:
      image_name: "{{ ctx().image_name }}"
      image_tag: "{{ ctx().image_tag }}"
      directory: "{{ ctx().directory }}"
      dockerfile_name: "{{ ctx().dockerfile_name }}"
      registry_host: "{{ ctx().registry_host }}"
      tls_verify: "{{ ctx().tls_verify }}"
    next:
      - when: "{{ succeeded() }}"
        publish:
          - build_status: Success
        do: post_result
      - when: "{{ failed() }}"
        publish:
          - build_status: Fail
          - result_message: "{{ result().stderr }}"
        do: post_result
  post_result:
    action: slack.post_message
    input:
      message: |
        Build_statue: {{ ctx().build_status }}
        Repository: {{ ctx().repository_url }}
        Image: {{ ctx().registry_host }}/{{ ctx().image_name }}:{{ ctx().image_tag }}
        {% if ctx().result_message is not none %}
        message:
        ```
        {{ ctx().result_message }}
        ```
        {% endif %}
      channel: "#general"
      username: bot_test
      webhook_url: "https://hooks.slack.com/xxxxxxxxxxxxxxxxxxxx"

pack の作成

最後に、作成した action と workflow を分類するための独自 pack cicd を作成します。

packs の作成は簡単で、pack の定義を行うメタデータファイル pack.yml を作成します。

pack.yml
---
name: cicd
description: my CICD custom pack
keywords:
  - cicd
version: 1.0.0
python_versions:
  - "3"
author: aaa
email: info@local.com
key description
name pack の名前
description pack の説明
keywords pack に関連するワードを list 形式で指定
version pack のバージョン
python_versions 対応する python version
author author 名
email author email

そして今まで作成したファイル群を以下のよう配置します。

cicd
├── actions
│   ├── build.sh
│   ├── build-workflow.yml
│   ├── build.yml
│   ├── git-clone.sh
│   ├── git-clone.yml
│   └── workflows
│       └── build-workflow.yml
└── pack.yaml

上記の cicd ディレクトリを st2client pod 内の /opt/stackstorm/packs に配置し、st2 pack install file:///opt/stackstorm/packs/cicd を実行することで独自 pack が登録できます。

root@st2-st2client-5fb9c79dcd-snlfr:/opt/stackstorm/packs# st2 pack install file:///opt/stackstorm/packs/cicd

        [ succeeded ] init_task
        [ succeeded ] download_pack
        [ succeeded ] make_a_prerun
        [ succeeded ] get_pack_dependencies
        [ succeeded ] check_dependency_and_conflict_list
        [ succeeded ] install_pack_requirements
        [ succeeded ] get_pack_warnings
        [ succeeded ] register_pack

+-------------+---------------------+
| Property    | Value               |
+-------------+---------------------+
| ref         | cicd                |
| name        | cicd                |
| description | my CICD custom pack |
| version     | 1.0.0               |
| author      | aaa                 |
+-------------+---------------------+

登録すると webUI の Actions からも追加した pack 内の action が確認できます。

image

packs を登録する際は開発環境から st2client pod 内にディレクトリ全体をコピーする必要があるのがやや面倒です。この部分は今後 k8s CRD で対応できるようにしてほしいところ。

ワークフローの実行

web UI から作成したワークフローを実行します。
web UI の Actions > build で build-workflow を選択してワークフローに渡すパラメータを入力を行います。この画面ではワークフローのメタデータで定義した parameter が表示されており、* が点いているものが必須パラメータとなります。また、notify や trace_tag など自動で追加されているパラメータもあります。

RUN を押すとワークフローの実行がキューに予約され、内部的にいずれかの st2actionrunner pod に割り当てられて実行されます。すべてのタスクが正常に完了すると、slack の該当 channel に build_status: success としてメッセージが投稿されます。

入力パラメータに不備があり途中のタスクが失敗した際は build_status: fail として失敗した理由が message に表示されます。これは失敗した action の stderr のエラーメッセージが表示されるようにしています。以下の例では git repository の URL を間違って指定したために git clone に失敗していることがわかります。

また、タスが完了した場合は registry UI よりコンテナイメージが正常に build & push されていることも確認できます。

ワークフローやアクションの実行履歴は web UI の History から確認できます。ここでは同じワークフローを入力パラメータを変更して実行などができます。

action の準備などが結構大変でしたが、他のワークフローツールと同様にユーザ独自で作成した一連の CI/CD ワークフローが実行可能なことが確認できました。

イベントの受信

st2 はイベント駆動型であるのでもちろん git repository に変更が生じた際に上記のワークフローを実行するように設定できます。今回の範囲では触れていませんが、st2 では以下のコンポーネントでイベントに対応します。

  • sensor, trigger
    • webhook 等のイベント受信を対応するコンポーネント
  • Rule
    • 受信したイベントの中身を分析、フィルタリングする。
    • 特定の条件を満たす際に action や workflow を実行する。

tekton, concourse との比較

OSS ワークフローツールはたくさんあり、今までの記事でも tekton, concourse といったクラウドネイティブなワークフローツールを扱ってきました。それでは各種ワークフローツールと st2 はどれを使えば良いのでしょうか。
もちろんそれぞれの使い勝手は目的やユースケース、環境の規模等により異なり単純な比較は難しいため、いくつかの観点から主観と独断に基づいて比較します。

ワークフローの可読性

\textrm{stackstorm} ≒ \textrm{concourse} ≒ \textrm{tekton}

ワークフロー自体はいずれも yaml 形式で記述できるため、それぞれの記法の差はあれど可読性に関してはそこまで差はないと思います。
強いて言うなら、 st2 では入力パラメータ等のメタデータと実際の処理を行うワークフロー部分が別ファイルに分離している一方、concourse や tekton では 1 つの yaml にまとめて記述する部分がそれぞれの特徴です。それを良いと捉えるかどうかは人によりますが。

機能の豊富さ

\textrm{stackstorm} ≒ \textrm{concourse} ≒ \textrm{tekton}

tekton, concourse はスクリプト形式で自由に処理を記述でき、必要に応じて独自の処理を行うコンテナイメージを作成して使用することができます。
stackstorm では処理自体は action としてシェルスクリプトや python スクリプトで記述できます。そのため、目的に合った処理を自作できるという点ではいずれも差は無いと考えられます。
tekton, concourse は処理自体をコンテナイメージで作成するので環境の依存性をある程度分離できますが、st2 はスクリプトで実装するため処理内容によってはその部分がネックになるかもしれません。

ワークフローの処理に関しては、st2 ではタスクのリトライ処理や条件分岐、並列などの機能がデフォルトで存在するので制御がやや楽になっています。また jinja2 の表式が使用できるので、本文の slack post action で使ったように特定の変数がセットされている場合のみ値を追加したり、if 文を使って input に渡すパラメータを変更したりするといった処理が楽に実装できます。これを tekton 等で実装しようとすると条件分岐を 1 つ 1 つ記述する必要があるので、記述量を削減しつつより多くのことを実現できるという点では st2 ワークフローの方が表現力は高いと言えます。

Web UI の使いやすさ

\textrm{stackstorm} > \textrm{tekton} ≒ \textrm{concourse}

どのツールにも web UI が備わっていますが、stackstorm の webUI では登録済みの action, pack, ワークフローの可視化、実行済みワークフローをパラメータを変更して再実行が可能であるため、他のツールと比べると使いやすいと感じます。
また、st2 では Role-Based Access Control (RBAC) によるユーザ管理機能があり webUI へのログインにも user, password の入力が求められますが、tekton ではその部分の対応は自分で行う必要があります。

学習コスト

\textrm{stackstorm} > \textrm{tekton} > \textrm{concourse}

stackstorm ではワークフローを使うまでに action, pack の概念や記述形式など覚えることが多いです。ワークフロー内の変数の受け渡し等でも yaql や jinja2 に基づいた独特な記法を用いるため、使いこなすまでに少し慣れが必要になります。
tekton, concourse でもワークフローの記述形式を覚える必要はありますが、一般的な yaml 記法で表現可能なため st2 と比較すると学習コストは低いかと思います。
上記の理由から学習コストに関しては上記の大小関係としました、

運用・管理の容易さ

\textrm{concourse} > \textrm{tekton} > \textrm{stackstorm}

stackstorm はマイクロサービスアーキテクチャであり、必然的にコンポーネント数も多いため運用・管理の面では結構な負担になりそうです。特に本格的な運用を意識する場合、st2 固有のコンポーネントだけでなくメッセージキュー (rabbitmq) やデータベース (mongoDB) の可用性等も考える必要があります。
コンポーネント数の多さが運用・管理の大変さに直結することを考えると st2 が最も管理が大変であると考えられるため上記の大小関係になるかと思います。

その他

st2 のアーキテクチャ

helm で st2 をインストールすると pod がたくさん作成されますが、これらは st2 アーキテクチャの各コンポーネントに対応しています。st2 はマイクロサービスアーキテクチャで設計されているため、役割毎にコンポーネントが分離されています。

また、helm でのインストールは High Availability 構成としてインストールされます。こちらについては https://docs.stackstorm.com/reference/ha.html にコンポーネント間の連携や役割がまとまっています。

st2 CLI を pod 外から実行する

st2 client CLI は st2client pod にデフォルトでインストールされていますが、pip を使えば他のマシン インストールすることもできます。

pip install st2client

client では st2 user の認証情報、および各種エンドポイントの設定が必要です。
username, password は webUI ログインに使用する認証情報が使用できます。
各種エンドポイントは helm install 時に svc として作成されており、この ClusterIP を使えばアクセスできます。

$ kubectl get svc
NAME                    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                 AGE
st2-st2api              ClusterIP   10.103.84.133    <none>        9101/TCP                                3d18h
st2-st2auth             ClusterIP   10.109.68.142    <none>        9100/TCP                                3d18h
st2-st2stream           ClusterIP   10.105.237.199   <none>        9102/TCP                                3d18h

client CLI は linux 実行ユーザのユーザディレクトリにある ~/.st2/config の情報を読み取るのでこれを作成します。

[credentials]
username = st2admin
password = [パスワード]

[api]
url = http://[st2api の ip]:9101

[auth]
url = http://[st2auth の ip]:9100

[stream]
url = http://[st2stream の ip]:9102

このように設定することで、pod 外部からでも st2 の各種 API を実行できます。

$ st2 pack list
+---------+---------+-------------------------+---------+------------------+
| ref     | name    | description             | version | author           |
+---------+---------+-------------------------+---------+------------------+
| chatops | chatops | ChatOps integration     | 3.8.0   | StackStorm, Inc. |
|         |         | pack                    |         |                  |
| cicd    | cicd    | my CICD custom pack     | 1.0.0   | aaa              |
| core    | core    | Basic core actions.     | 3.8.0   | StackStorm, Inc. |
| default | default | Default pack where all  | 3.8.0   | StackStorm, Inc. |
|         |         | resources created using |         |                  |
|         |         | the API with no pack    |         |                  |
|         |         | specified get saved.    |         |                  |
| git     | git     | Git SCM                 | 2.0.0   | StackStorm, Inc. |
| linux   | linux   | Generic Linux actions   | 3.8.0   | StackStorm, Inc. |
| packs   | packs   | Pack management         | 3.8.0   | StackStorm, Inc. |
|         |         | functionality.          |         |                  |
| slack   | slack   | Slack Chat integrations | 2.1.0   | StackStorm, Inc. |
+---------+---------+-------------------------+---------+------------------+

buildah が実行可能な st2actionrunner イメージの作成

ワークフローの中でコンテナイメージをビルドするにはビルドコマンドを実行する action を作成する必要があります。
しかし、k8s では st2actionrunner pod 内で action が実行されるため、docker や containerd のような docker engine 等の動作を必要とするコマンドでビルドすることができません。
また、スクリプトとして実行されるため、tekton の際に使用した kaniko を使ってビルドすることもできません。
そのため、docker engine 等の daemon を必要とせずコマンドラインか実行可能なビルドツール buildah を使ってイメージをビルドすることを考えます。

https://buildah.io/

もちろん st2actionrunner にはデフォルトで入っていないのでインストールする必要があるのですが、ここでもう一つ問題があります。helm でインストールする場合の st2actionrunner pod で使用されているイメージは st2-docker のレポジトリの Dockerfile から作成されており、ベースイメージには ubuntu focal (20.04) が使用されています。一方で buildah は ubuntu 22.10 移行のバージョンをサポートしておりそのままではインストールできません。

じゃあベースイメージを ubuntu 20.10 移行にした st2actionrunner をビルドすればいいのでは思いますが、現時点で st2 本体が ubuntu 20.04 までしか対応してなく、実際 ubuntu 22.04 で Dockerfile のビルドを試してみると apt で st2 をインストールする箇所で失敗します。

色々見た結果、rockylinux 8 系であれば st2 本体、buildah の両方をサポートしているので、ベースイメージを rockylinux 8 系にして st2actionrunner を自前でビルドすれば使えるようになります。
ただし上記の Dockerfile は ubuntu を前提としているため、rockylinux で動作するように Dockerfile の中身を書き換える必要があります。
書き換えた内容は以下の github に置いてあります。

https://github.com/git-ogawa/zenn_resource/tree/main/articles/st2/actionrunner_image

また、このままだと pod 内で buildah build を実行した際に以下のエラーが発生します。

Error: mount /var/lib/containers/storage/overlay:/var/lib/containers/storage/overlay, flags: 0x1000: permission denied
WARN[0000] failed to shutdown storage: "mount /var/lib/containers/storage/overlay:/var/lib/containers/storage/overlay, flags: 0x1000: permission denied"

おそらく pod の security context 的に弾かれているので、deployment のマニフェストに securityContext を追加します。
privileged: true に設定するのはセキュリティ的によろしくないですが、今回は検証目的なので良しとします。

spec:
    ...
+    securityContext:
+      privileged: true

問題はもう一つあります。st2actionrunner pod 内は通常 uid=1000 の stanley ユーザで action が実行されますが、これで buildah を実行すると以下のエラーが発生します。

WARN[0000] running newgidmap: exit status 1: newgidmap: write to gid_map failed: Operation not permitted
WARN[0000] /bin/newgidmap should be setgid or have filecaps setgid
WARN[0000] Falling back to single mapping
WARN[0000] Error running newuidmap: exit status 1: newuidmap: write to uid_map failed: Operation not permitted
WARN[0000] /bin/newuidmap should be setuid or have filecaps setuid
WARN[0000] Falling back to single mapping
...
Error: creating build container: copying system image from manifest list: writing blob: adding layer with blob "sha256:96526aa774ef0126ad0fe9e9a95764c5fc37f409ab9e97021e7b4775d82bf6fa": processing tar file(potentially insufficient UIDs or GIDs available in user namespace (requested 0:42 for /etc/shadow): Check /etc/subuid and /etc/subgid if configured locally and run podman-system-migrate: lchown /etc/shadow: invalid argument): exit status 1

おそらく実行ユーザの権限諸々で弾かれているので、root ユーザで action を実行するように変更します。

st2 では st2.conf という config ファイルで様々な設定を制御でき、system_user セクションで実行ユーザを指定できます。config ファイルの例
helm でインストールすると上記設定ファイルに相当する configmap が作成され、st2actionrunner の pod にマウントされる形式になっています。
そのため、実行ユーザを変更する configmap を作成し、st2actionrunner pod にマウントの設定を追加すればよいということになります。

configmap

apiVersion: v1
data:
  st2.user.conf: |
    [system_user]
    user = root

    [api]
    allow_origin = '*'
kind: ConfigMap
metadata:
  annotations:
    description: Custom StackStorm config which will apply settings on top of default st2.conf
    meta.helm.sh/release-name: st2
    meta.helm.sh/release-namespace: st2
  labels:
    app: st2
    app.kubernetes.io/managed-by: Helm
    chart: stackstorm-ha-0.110.0
    heritage: Helm
    release: st2
    tier: backend
    vendor: stackstorm
  name: st2-user-config
  namespace: st2

deployment pod でマウントする

        volumeMounts:
        - mountPath: /etc/st2/st2.user.conf
-          name: st2-config-vol
+          name: st2-user-config-vol
          subPath: st2.user.conf
      volumes:
+      - configMap:
+          name: st2-user-config
+        name: st2-user-config-vol

まとめ

以上をまとめると、st2actionrunner 用のコンテナイメージをビルドし、既存の st2actionrunner deployment マニフェストを以下のように編集すれば動くようになります。

spec:
  containers:
    - ....
-      image: stackstorm/st2actionrunner:3.8
+      image: myactionrunner:1.0
     ...
+    securityContext:
+      privileged: true
    ...
    volumeMounts:
      - mountPath: /etc/st2/st2.user.conf
-       name: st2-config-vol
+       name: st2-user-config-vol
          subPath: st2.user.conf
    ...
  volumes:
+   - configMap:
+     name: st2-user-config
+     name: st2-user-config-vol

長い道のりでしたが、これでようやくワークフロー内でコンテナイメージのビルドが実行できるようになります。

おわりに

st2 を k8s クラスタにインストールしてワークフローを実行する手順についてまとめました。
st2 は自動化の機能が豊富である半面、学習コストや管理の複雑さが大きいといった印象を感じます。
tekton や concourse と同じ一連の処理を実行するワークフローツールというカテゴリには分類されますが、それぞれ得意とする処理が異なるため必要に応じて使い分けていくのが良いでしょう。
例えばコンテナイメージのビルドを含む CI/CD プロセスや k8s クラスタへのデプロイに関しては、CI/CD に特化したクラウドネイティブな tekton や concourse を使う方が楽かと思います。
一方で、より汎用的なワークフロー処理は Automation に特化した st2 の得意領域であるため、st2 で検知や対応処理を組むのが良いと思われます。

Discussion