AWS Step Functions入門

## はじめに

皆さんStep Functions使ってますか？
今回はStep Functionsがいかに簡単で可能性があるのか、を伝えるために入門記事を書いてみようと思います。
Step Functionsは基礎部分を理解すればある程度使いこなすことができるので、皆さんぜひ使ってみてください。

## 作るステートマシンの全体像

今回はサンプルとして複数のバケットを空にするようなステートマシンを作成します。
画面からバケットを空にするのはボタンを押すだけなので簡単ですが、自動化しようと思うとコードを書かないといけないので面倒です。
GUIを使ってステートマシンを作って、定期起動すれば自動化できるので、ノーコードで実現できるので便利ですよね？ね？？？

## 下準備

まず、名前は何でもいいのでバケットを2つ作成してください。
また、それぞれのバケットに適当なファイルを保存してください。

今回は以下2つのバケットを作成して、それぞれのバケットにファイルを3つ作成しました。

* step-functions-sample-1
* step-functions-sample-2

![Snag_deb64d2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/4424964a-7c1f-c47c-a218-4228e33e2f28.png)
![Snag_deb727e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/33618e21-b38d-9e24-d9f8-757c6e4131c6.png)

## Step Funcctionsのベースを作成

「Step Functions」のページから「ステートマシンの作成」をクリックします

![Snag_ded7dbf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/78e46086-5e9b-f35c-9d6e-4b13aab27f9e.png)

以下の画像の通りに選択して、「次へ」をクリックします。
![Snag_df61092.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/77fd068a-3b81-7e1f-6298-c15d748ae0b9.png)

左上の検索欄に「ListObject」と入力します。
すると絞り込まれた結果から「ListObjectsV2」をドラッグアンドドロップして、「最初の状態をここにドラッグ」の部分に移動させます。
![Snag_df7a0f6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/32fc46a1-c84b-b53d-d252-8ca4d939d07e.png)

その後、右下の「APIパラメータ」の部分を修正し、作成したバケット名を入力します。
その後、右上の「次へ」をクリックします。
![Snag_df84813.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/5fa4b469-edd6-786c-f654-79ec688d767a.png)

右下の「次へ」をクリックします。
![Snag_df8ade2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/1ef1d99c-fe88-3059-2625-dec3ca4be278.png)

名前を適宜変更し、ルールは新規作成を選択して右下の「ステートマシンの作成」をクリックします。
![Snag_df96c8e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/33ca03dd-6b13-762c-c4ea-74f0c1a54f95.png)

IAMロールのリンクをクリックし、該当のロールに「AmazonS3FullAccess」をアタッチしてください。
※今回はサンプルなので強い権限を当てていますが、本来は権限を制限する必要があるのでご注意ください。
![Snag_dfc4bb9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/1c2fcf4b-6f4b-eb1c-3210-b51d27f98569.png)

Step Functionsの画面に戻り、「実行の開始」ボタンを押します。
![Snag_dffb8ec.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/a0a7ef28-bf95-4d0d-610a-33a6b2ea7dbe.png)

表示される画面はデフォルトのまま、右下の「実行の開始」をクリックします。
![Snag_e0031f4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/0a0a74c5-d5be-f967-6e96-2533157617f3.png)

以下の画像の通り、グラフインスペクター内の「ListObjectsV2」の部分をクリックし、「ステップ出力」をクリックします。
すると、該当のS3バケットの中身が表示されます。
(Keyの部分がオブジェクト名になってます)
![Snag_e78b891.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/2c15e3cf-7add-37d0-a7ad-1da7cb701c1d.png)

## S3バケット内のオブジェクトを削除できるように編集する

では、このステートマシンを編集しましょう。
右上の「ステートマシンの編集」をクリックします。
![Snag_e799c5a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/54abe9d2-b279-9aa4-2941-e930fecc0013.png)

「Workflow Studio」をクリックします。
![Snag_e79f96e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/61b5e5ae-891e-7c79-fa4e-e7d00c42bb2a.png)

今回から念のため、タイムアウトを設定します。
ステートマシンの実行が想定を超えて実行されることを防ぐためです。
今回は600(10分)を入力します。
![Snag_e7af67b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/4f980f70-d1b7-2b4b-1099-e98760ec949f.png)

「フロー」から「Map」を選択します。
ListObjectsV2で取得したオブジェクトの一覧を削除するため、オブジェクト一覧に対してループ処理を実行したいからです。
※Mapはデフォルトでは原則処理が並列で実行されます。順次実行も設定で可能です。
[マップ](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/amazon-states-language-map-state.html)
![Snag_e7b5bfc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/06757b0a-0bcb-16b3-5376-64bc90c6544b.png)


先ほどListObjectsV2の出力を確認しましたよね。
出力としては「Contents」の中に配列が格納されており、その配列の「Key」がオブジェクト名でした。
![Snag_e7e85bc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/ec30d078-bbfc-9f3d-4ed0-95ebef3463d7.png)

よって、配列項目へのパスに「$.Contents」と入力します。
![Snag_e7f94ac.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/7426ca2c-e601-6922-7a96-21b5e7443951.png)

次に、Map内の処理を追加します。
今回はオブジェクトを削除したいので、「DeleteObject」を選択して、ドラッグアンドドロップします。
APIパラメータにはバケット名は固定値を入力し、Keyの部分は下記画像のようにしてください。
その後、「適用して終了」をクリックします。
![Snag_e80c58b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/5ecba282-9727-65af-9aa2-b471efa1b224.png)

ここで、`"Key.$": "$.Key"`ってどこから来たのか、と思われるかもしれません。
簡単に説明すると、入力値を動的に参照させたい場合、その属性名の末尾に「`.$`」をつけ、属性値の先頭は「`$.`」で始めるJson Pathで指定する、ということになります。
今回は「Key」の部分を入力から動的に値を決めたかったので、`"Key.$": "$.Key"`のようにしています。
[キーバリューペア](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/input-output-inputpath-params.html#input-output-parameters)

なお、値の「Key」については、以下の画像の通り配列の要素の「Key」の部分がオブジェクト名だとわかっているので、その値を入力しています。
![Snag_e7e85bc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/ec30d078-bbfc-9f3d-4ed0-95ebef3463d7.png)

右上の保存を押します。
![Snag_e8dc1e2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/30343b4f-a3ab-800d-b3bc-4bc96785e790.png)

「保存を続行します」をクリックします。
![Snag_e8e56b0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/4510422b-eb4d-cf07-65eb-8c090cb8c5d9.png)

では、もう一度ステートマシンを実行してみます。
![Snag_e927427.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/b928800b-593c-9a67-387a-72b39147836e.png)

![Snag_e928638.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/a2161011-2bec-b07b-7b00-97e700a97719.png)

実行結果のグラフインスペクターが以下のようになっていればOKです。
![Snag_ea0ccb3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/3acd0cfd-7411-d3bc-4fa5-0b4f8dbde83c.png)


しっかり該当のバケットのデータが削除されていますね。
![Snag_e93bbda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/e5a3a73c-67d2-bb65-da3e-2defddc4cc00.png)

## バケット名を動的に変えることができるように編集

先ほどまで、バケット名を固定で入れていましたが、バケット名を入力値として与えて、動的に変更できるように修正しましょう。
また、複数のバケットを指定できるようにもしてみましょう。

入力値のイメージは以下となります。

```
{
  "bucketList": [
    {"bucketName": "step-functions-sample-1"},
    {"bucketName": "step-functions-sample-2"}
  ]
}
```

前回と同様の手順で「Workflow Studio」を開いてください。

フローからMapを選択し、ステートマシンの先頭に挿入します。
その後、項目配列へのパスは「$.bucketList」と入力します。
その後、下のListObjectsV2をMap内に入れます。
![Snag_ea1f70a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/c6e4a89f-46fe-2449-42ca-0b92808ab1c9.png)

ListObjectsV2を選択し、APIパラメータを下記画像のように変更します。
また、二個目のMapを一つ目のMapの中に入れます。
![Snag_ea337d7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/7c5439b9-2a0b-9f9b-69f2-5239673bab36.png)

二個目のMapを選択して、項目配列へのパスが「$.Contents」となっていることを確認します。
![Snag_efb376e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/25eeec2c-95f8-2c8c-eb08-ac36b5f3dcea.png)

次に、Mapの入力欄を以下のように編集して下さい。
この部分の設定を変更することで、Mapへの入力データをカスタマイズすることができます。
ListObjectV2の配列内にはバケット名のデータがないので、Mapで処理した際に都合が悪いです。(理由はオブジェクトを削除する際にバケット名も必要なため)
よって、入力内容をカスタマイズして、バケット名とオブジェクトキーの両方を配列の処理に含めることができるように修正します。

```
{
  "bucketName.$": "$.Name",
  "ContextValue.$": "$$.Map.Item.Value"
}
```
![Snag_efd20bf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/54154f81-77cd-c382-c000-0239695026c3.png)

ここで、`"bucketName.$": "$.Name",`と`"ContextValue.$": "$$.Map.Item.Value"`がどこから出てきたのか、と疑問に思われるかもしれません。
順番に解説します。

まず、`"bucketName.$": "$.Name",`ですが、ListObjectsV2の出力結果は以下のようになっています。

```
{
  "name": "ListObjectsV2",
  "output": {
    "Contents": [
      {
        "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
        "Key": "1-1.txt",
        "LastModified": "2022-04-17T13:22:56Z",
        "Size": 0,
        "StorageClass": "STANDARD"
      },
      {
        "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
        "Key": "1-2.txt",
        "LastModified": "2022-04-17T13:22:57Z",
        "Size": 0,
        "StorageClass": "STANDARD"
      },
      {
        "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
        "Key": "1-3.txt",
        "LastModified": "2022-04-17T13:22:57Z",
        "Size": 0,
        "StorageClass": "STANDARD"
      }
    ],
    "IsTruncated": false,
    "KeyCount": 3,
    "MaxKeys": 1000,
    "Name": "step-functions-sample-1",
    "Prefix": ""
  },
  "outputDetails": {
    "truncated": false
  }
}
```

出力結果をどうやって確認するかというと、ステートマシンの実行結果から見ることができます。
![Snag_f015e13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/6681e71b-4e46-5c4b-ef43-0c2b745d2d14.png)

注目するのは「output」の部分で、この内容が次のinputになります。
なので、次のインプットであるMapにはこのoutputの内容が入るわけです。
そして、今回利用したいのはバケット名なので、outputの中のNameという部分がバケット名に当たります。
ということから、`"bucketName.$": "$.Name",`となっていたわけです。

次に`"ContextValue.$": "$$.Map.Item.Value"`の部分です。
詳細は以下のリファレンスを参照頂きたいのですが、今回のように書けば、入力値をContextValueの中に入れることができます。
[マップ状態のコンテキストオブジェクトデータ](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/input-output-contextobject.html#contextobject-map)
実質、入力値をマージすることができるというわけですね。

最後に、DeleteObjectの内容を以下のように修正します。
修正後、適用して終了、その後、保存してください。

```
{
  "Bucket.$": "$.bucketName",
  "Key.$": "$.ContextValue.Key"
}
```
![Snag_f0c16f2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/944b0abc-5780-68f9-cd82-ac0cfd72a519.png)


再度入力値を以下のようにして実行します。
**この際に、対象とするバケットに必ずオブジェクトが1つ以上存在するようにしてください。**
**バケット内にオブジェクトが存在しないとエラーになります。**
![Snag_f0c371d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/386b886c-3247-a7e1-37ff-6dfd4ac80c81.png)

グラフインスペクターが以下のようになっていればOKです。
![Snag_f0d13c1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/1ea02c7d-d1ff-eae9-9b55-151804eba1ea.png)

## IF条件を追加する

先ほど、バケット内にオブジェクトが1件も存在しないとエラーとなる、と書きました。
実際に実行してみると、以下のようなエラーになります。

```
{
  "error": "States.ReferencePathConflict",
  "cause": "Unable to apply step \"Contents\" to input {\"IsTruncated\":false,\"KeyCount\":0,\"MaxKeys\":1000,\"Name\":\"step-functions-sample-1\",\"Prefix\":\"\"}"
}
```

バケット内にオブジェクトが1件もないとListObjectsV2のouptutにContentsが存在しなくなるので、参照エラーが起きている、ということです。
そこで、IF条件を追加してContentsが無ければ次の処理をスキップするような条件に変更してみます。
再度Workflow Studioを開いてください。

フローから「Choise」を選択し、ListObjectsV2の後に差し込みます。
![Snag_f15ffef.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/e8330515-f8bc-222a-83be-49a909423564.png)

次にChoiseの「Rule #1」の下の枠に、「Pass」を入れます。
![Snag_f16b276.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/0a9ea101-84c5-c65c-7823-feed8d21b6a0.png)

Choiseを選択し、Choise Rules内のRule #1の「Add conditions」をクリックします。
![Snag_f18c345.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/99e112d4-b7d0-50c8-f481-9a6c0594b024.png)

以下の画像のように修正して、「条件を保存する」をクリックします。
これで、「Conditionsが存在すれば」という条件になります。
![Snag_f1958fd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/08c19250-e94f-2661-a060-8e1541a28bd1.png)

次に、「Then next state is:」の部分を「Map」に変えます。
![Snag_f1b2bf8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/b63b209d-cfdd-2630-3b52-f7b5750b5dc8.png)

次に、「Default rule」の「default state」をPassに変えます。
するとChoiseの分岐が以下の画像のようになると思います。
![Snag_f1cbbdf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/3a463262-12fb-273a-f80b-cd017b769b51.png)

この状態で再度保存して実行してみてください。
グラフインスペクターが以下のようになっていればOKです。
今回はバケット内のオブジェクトが1つもない状態で実行したので、ただしくChoiseで分岐されていることが分かります。
![Snag_f1eec5c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/ee10f39a-292a-b18d-4a27-a42276b72589.png)

## ループ処理を追加する

これまでの手順でバケットの中身を空にできそうですよね？
実はまだ足りません。
ListObjectsV2のリファレンスを見ると以下の文章があります。
[ListObjectsV2](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/API_ListObjectsV2.html)
>Returns some or all (up to 1,000) of the objects in a bucket with each request. 

要するに、一回のリクエストで上限1000件までしか返らないよ、ということです。
つまり、現状の構成だとバケット内に1000個以上のオブジェクトが存在すると、正しくバケットを空にできない、ということになります。
どうするかというと、ListObjectsV2のレスポンス内容にIsTruncatedという属性値がありました。
IsTruncatedがtrueの場合、レスポンスに全件含まれていないという意味になるので、この値で条件分岐すればよさそうです。

まず、ListObjectsV2の出力の「ResultPath を使用して元の入力を出力に追加」にチェックを入れて、「Combine original input with result」を選択、値は「$.ListObjectsV2Result」と入力します。
![Snag_f6b7725.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/7ab2bc6e-d63f-f67f-8abd-e6e476c25ce9.png)


この設定はinputの内容とステートの実行結果を合わせて次のステートに渡す必要がある際に利用します。
今回のステートの実行結果はListObjectsV2Result以下に格納されます。
実際のoutputは以下のようになります。
inputのbucketNameと実行結果がListObjectsV2Result以下に入った状態になっているのがわかると思います。

```
{
  "name": "ListObjectsV2",
  "output": {
    "bucketName": "step-functions-sample-1",
    "ListObjectsV2Result": {
      "Contents": [
        {
          "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
          "Key": "1-1.txt",
          "LastModified": "2022-04-17T16:22:02Z",
          "Size": 0,
          "StorageClass": "STANDARD"
        }
      ],
      "IsTruncated": false,
      "KeyCount": 6,
      "MaxKeys": 1000,
      "Name": "step-functions-sample-1",
      "Prefix": ""
    }
  },
  "outputDetails": {
    "truncated": false
  }
}
```

次にChoiseのRule #1を以下の画像のように修正します。
inputの値の構造が変わっているので、参照する値も変わるからです。
![Snag_f6e3d4a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/d2dd64b5-2e56-d307-ef41-f4a225708636.png)


次にMapの項目配列へのパスを修正します。
inputの構造が変わったので修正が必要です。
![Snag_f77a2a0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/648f924d-f912-9bc4-61d3-f497ec3ec8fb.png)


また、出力の「ResultPath を使用して元の入力を出力に追加」にチェックを入れて、「Discard result and keep original input」を選択します。
今回はタスクの実行結果は次に渡す必要がないので、「Discard result and keep original input」を選択しています。
![Snag_f701d55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/5a9cee36-40c2-c514-3681-278c5c5c5cc0.png)


次にChoiseをMapの下に差し込みます。
![Snag_f711f25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/c9377549-4140-076a-20e1-3476648a335e.png)

Choise(1)のRule #1の条件を以下のようにします。
Variable: `$.ListObjectsV2Result.IsTruncated`
![Snag_f72ac1e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/aa6fbbba-dd78-99fb-e602-75a2ba33a984.png)


Rule #1の次のステートはListObjectsV2とします。
![Snag_f74a7fd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/6c318a86-dbd3-94fc-abb9-16e779f7c626.png)


Default stateはPassとします。
![Snag_f74eaf2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/d2ade3bd-342c-a0cf-658e-a2df83564527.png)

最終的には以下のようになります。
![Snag_f7a0596.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/a22e376d-f3cb-91c5-fda2-ddea73ccf828.png)

これで保存して実行してみてください。
グラフインスペクターが以下のようになっていればOKです。
![Snag_f555ca3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/192873/fc3b7b67-426e-d917-9993-aa79d028d9a5.png)

## 最後に

バケットを空にする実装はLambdaで実装すればステートマシンを作るよりももっと簡単に実現できると思います。
実際にStep Functionsが生きてくるのはLambdaでは対応が難しいような処理の待ち時間が発生するようなユースケースや複数のLambdaの状態管理が必要になる場合だと思います。
[ユースケース](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/welcome.html#application)
[ユースケース](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/use-cases.html)

ただ、今回の記事を通してざっくりとでもStep Functionsの使い方、魅力が伝わればいいなと思います。