runnのカスタムランナーでAWS CLI の高レベルS3コマンド動かしてみた

runnのカスタムランナーでちょっと実用的なS3アクセスをやってみました。
2024.03.05

小ネタ:runnのカスタムランナーでFizzBuzzやってみた | DevelopersIO

でまずは試しに使ってみましたが、もうちょっと実用的なことも出しておかないなーと思い、やってみました。

何をやるか?

我々のプロダクトでは、API実行した結果、S3にファイルを配置するような処理があります。

こういったシナリオをテストしようと思うと、「今の」runnには専用のランナーがありません。なので、S3をアレコレするカスタムランナーを作ってみようと思います。

方針

カスタムランナーでは既存のランナーを組み合わた処理しか行えないため、目的を達成するにはどうするかを考えたところ、今回はシンプルにAWS CLIを使ったS3への高レベルコマンドをラップする形で作ってみようと思います。

AWS CLI で高レベル (S3) コマンドを使用する - AWS Command Line Interface

できたもの

結果できたカスタムランナーの定義がこちらです。

custom-s3-runner.yaml

desc: AWS CLI S3 High-level command runner
if: included
vars:
  profile: '{{ parent.params.profile }}'
  command: '{{ keys(parent.nodes)[0] }}'
  paths: '{{ parent.nodes[keys(parent.nodes)[0]].paths }}'
  options: '{{ parent.nodes[keys(parent.nodes)[0]].options }}'
steps:
  -
    exec:
      command: "aws --profile {{ vars.profile }} s3 {{ vars.command }} {{ vars.paths }} {{ vars.options }}"
    bind:
      exit_code: current.exit_code
      stdout: current.stdout
      stderr: current.stderr

このカスタムランナーを使用する側のシナリオはこちらです。

s3-runner.yaml

desc: S3アクセスのカスタムランナーを作ってみる
runners:
  s3:
    path: s3-runner.yaml
    params:
      profile: myprofile
steps:
  listObjects:
    desc: "特定バケットのオブジェクトリストを取得する"
    s3:
      ls:
        paths: s3://runn-s3-runner-sample
        options: '--recursive'
    test: |
      current.exit_code == 0
    dump: current.stdout
  testExistentObject:
    desc: "特定オブジェクトの存在を確認する"
    s3:
      ls:
        paths: s3://runn-s3-runner-sample/some-object.txt
    test: |
      current.exit_code == 0
  testNonExistentObject:
    desc: "特定オブジェクトが存在しないことを確認する"
    s3:
      ls:
        paths: s3://runn-s3-runner-sample/nonexistent-object.txt
    test: |
      current.exit_code != 0
  GetObject:
    desc: "S3のオブジェクトを取得する"
    s3:
      cp:
        paths: s3://runn-s3-runner-sample/some-object.txt /tmp/some-object.txt
    test: |
      current.exit_code == 0
  CatObject:
    desc: "取得したオブジェクトの内容を確認する"
    exec:
      command: 'cat /tmp/some-object.txt'
    test:
      current.stdout == 'foo'

実行した結果がこちらです。外部コマンドを使うexecランナーを使うため、--scopes runn:execオプションを付けてrunnを実行します。

$ runn run --scopes run:exec custom-s3-runnder.yaml --verbose
=== S3アクセスのカスタムランナーを作ってみる (custom-s3-runnder.yaml)
    --- (0) ... ok
2024-03-04 09:27:20          0 any-object.txt
2024-03-03 16:32:18          0 some-object.txt

        === AWS CLI S3 runner (s3-runner.yaml)
            --- (0) ... ok
    --- (0) ... ok
        === AWS CLI S3 runner (s3-runner.yaml)
            --- (0) ... ok
    --- (0) ... ok
        === AWS CLI S3 runner (s3-runner.yaml)
            --- (0) ... ok
    --- (0) ... ok
        === AWS CLI S3 runner (s3-runner.yaml)
            --- (0) ... ok
    --- 取得したオブジェクトの内容を確認する (CatObject) ... fail
        Failure/Error: test failed on "S3アクセスのカスタムランナーを作ってみる".steps.CatObject "取得したオブジェクトの内容を確認する": condition is not true
        
        Condition:
          current.stdout == 'foo'
          │
          ├── current.stdout => ""
          └── "foo" => "foo"
          
        Failure step (custom-s3-runnder.yaml):
        38   CatObject:
        39     desc: "取得したオブジェクトの内容を確認する"
        40     exec:
        41       command: 'cat /tmp/some-object.txt'
        42     test:
        43       current.stdout == 'foo'



1 scenario, 0 skipped, 1 failure

S3のオブジェクトの存在確認および、取得して内容を確認することができています。

最後のステップでは、あえて中身が空のファイルを使って、テストでエラーになるようにしています。

解説

今回のカスタムランナーのポイントは以下のとおりです。

  • カスタムランナーの定義時と実行時に指定する内容を分別する
    • profile: 複数回実行しても同じものを使う事が多いため、カスタムランナーの定義で指定させる
    • commandpathsoptions: 実行時に毎回変わるため、実行時変数で指定させる
  • カスタムランナー定義、実行時に指定した値は、定義側で取得方法を使い分ける
    • カスタムランナー定義時にparamsで指定した値
      • {{ parent.params.<key> }}で参照できる
    • カスタムランナー実行側にてmapで指定した値
      • keys(parent.nodes)でキーリストが取得できる
      • parent.nodes.<key>でmapの値を取り出せる
        • parent.nodes[<key>]でも可能
      • これらを組み合わせることで柔軟な表現が可能になる
  • 実行結果を使用側で参照できるようbindしておく
    • exit_codestdoutstderrといったexecコマンドの結果を、カスタムランナー使用側で参照するには、bindで束縛する必要があります

まとめ

ある程度実用的なカスタムランナーも、比較的簡単に定義して使えることがわかりました。

ただ、定義するカスタムランナーに「直感的で使いやすいAPI」をつけるのは、設計者の腕の見せ所だなとも感じました。今回の例だと、あくまでAWS CLIのS3高レベルコマンドをラップしただけではありますが、カスタムランナーに与える値を、mapのキー、値どちらで渡したほうがより自然か?みたいな考え方は必要でした。

また、「これはカスタムランナーとして定義すべきものなのか?」も同時に考慮が必要だとも感じました。「DSLを作れる」というのは強い力ですが、ともすれば独りよがりなものにもなってしまいがちなものです。今後活用するにしても、使用箇所を見極めつつ、使ってみてはやっぱりやめたほうがいいとか、そういう試行錯誤していこうと思います。