[Step Functions] ステートマシンで「日付」を取り扱う方法

Lambdaを使わなくても何とかなりますが、時にはLambdaのチカラを借りることも大切です。
2022.12.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

みなさん、こんにちは!
福岡オフィスの青柳です。

AWS Step Functionsのステートマシンで「日付」を取り扱いたいことってありませんか?

例えば、以下のような場面があるのではないでしょうか。

  • ファイル名のプレフィックス・サフィックスに処理日を使いたい
  • ログに現在の日付を記録したい
  • 特定の日付をキーにしてデータベースを検索したい
  • etc.

今回は、Step Functions内で「日付」を取り扱う方法について考えてみました。

方法1: EventBridge Schedulerから受け取ったタイムスタンプを加工する

せっかくStep Functionsを使っているのですから、できれば「ノーコード」で実現したいですよね。

ということで、コードを書かなくても日付を取得できる方法が何か無いか調べてみたところ、EventBridge Schedulerからステートマシンを呼び出す場合、呼び出した日時をステートマシンの「入力」にセットできることが判りました。

Adding context attributes - EventBridge Scheduler

EventBridge Schedulerでターゲットを指定する際に、予約されたキーワード<aws.scheduler.scheduled-time>をターゲットへの入力ペイロードに含めることで、EventBridge Schedulerがスケジュールを実行した日時を渡すことができるのです。

上図のように設定されたEventBridge Schedulerから呼び出されたステートマシンには、最初に実行されるステートの「入力」に以下のようなJSONがセットされます。

{
  "Time": "2022-12-01T12:34:56Z"
}

この値をそのまま使ってもよいのですが、今回はStep Functionsに用意された「組み込み関数」を使って、タイムスタンプ文字列から「日付」部分だけを切り出してみます。

ステートマシンを作成する

下図のように2つの「Pass」ステートを使って文字列の切り出しを行います。

「Pass」ステート (一つ目)

一つ目のPassステートでは、Parametersフィルターを以下のように設定します。

文字列操作の組み込み関数States.StringSplitを使って、区切り文字「T」を指定して文字列を分割します。

分割された文字列は、配列の要素として格納されて、以下のように出力されます。

{
  "SplittedString": [
    "2022-12-01",
    "12:34:56Z"
  ]
}

「Pass」ステート (二つ目)

二つ目のPassステートでは、Parametersフィルターを以下のように設定します。

一つ目のステートで出力された配列から、「0」番目の要素を抜き出して「Date」の値とします。 (「1」番目の要素は捨てます)

二つ目のステートの出力結果は以下のようになります。

{
  "Date": "2022-12-01"
}

これで、最初にEventBridge Schedulerから入力されたタイムスタンプの文字列を、ステートマシンで加工して「日付」部分のみを取り出すことができました。

取得した「日付」文字列を使って処理を行う

次のような処理を定期的に実行して、結果をS3バケットに格納するステートマシンを作成してみます。

「CloudWatchのDescribeAlarmHistoryAPIを呼び出して、アラーム発報の履歴を出力する」

何故こんなことをやりたいのか?
それは、CloudWatchアラームの履歴って2週間しか保存されないので、2週間毎に履歴を出力して残しておきたいと思ったためなのです・・・

ステートマシンの続きを作成する

それはともかく、ステートマシンの構成は以下のようになります。

最初に作成したステートマシンに対して、「CloudWatch: DescribeAlarmHistory」「S3: PutObject」の2つのステートを追加しています。

「CloudWatch: DescribeAlarmHistory」ステート

「設定」タブで、下図のように「APIパラメータ」を指定します。

今回は「アクションを実行した履歴」のみを出力したいため、APIのパラメーター「HistoryItemType」に「Action」を指定しています。

次に、「出力」タブを下図のように設定します。

デフォルトの挙動ですと、ステートの出力は「APIの実行結果」で上書きされます。

今回の場合、後続の「S3: PutObject」でオブジェクトのキー名に「Date」を使用したいのですが、そのためには一つ目・二つ目のステートで取得・加工した「Date」の値を「S3: PutObject」へ持ち越す必要があります。

これを実現するために「ResultPath」フィルターを使用します。

「Combine original input with result」を選択することで、元の入力データを残しつつ、このステートの実行結果を指定したJSONキーの配下に追加することができます。

この設定によって、ステートの出力結果は以下のようになります。

{
  "Date": "2022-12-01",
  "TaskResult": {
    "AlarmHistoryItems": [
      << ここにDescribeAlarmHistoryの実行結果が入ります >>
    ]
  }
}

「S3: PutObject」ステート

「設定」タブで、下図のように「APIパラメータ」を指定します。

各パラメーターの説明は以下の通りです。

  • Bucket: 出力先のS3バケット名を指定
  • Key: 組み込み関数Status.Formatを使って「result_<日付>.json」という文字列を指定
  • ContentType: JSON形式テキストであることを指定
  • Body: 前のステートの実行結果がTaskResultに格納されているため、それをそのまま出力

これらのパラメーターを指定することで、S3のPutObjectAPIを使って「CloudWatch: DescribeAlarmHistory」の実行結果をJSON形式でオブジェクトに書き込むことができます。

補足 (IAMロールの設定)

このステートマシンでは「CloudWatch」「S3」の各APIを呼び出しますので、ステートマシンに割り当てられたIAMロールに以下のポリシーを追加しておく必要があります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "cloudwatch:DescribeAlarmHistory",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::<出力先S3バケット名>/*"
        }
    ]
}

方法2: Lambda関数を使って現在の日付を取得する

「方法1」によって、ステートマシンで「日付」をノーコードで取得して、処理 (S3オブジェクト作成) のパラメーターとして使用することに成功しました。

ただ、いくつか課題も見えてきます。

  • 日本時間ではなくUTCになっている (EventBridge Schedulerから渡すことができるタイムスタンプがUTCであるため)
  • 現在の日時しか取得できない (前日とか1週間前とかの日付を使いたくなることってないですか?)
  • EventBridge Schedulerから呼び出した場合しか使えない (手動で実行など他の手段でも日付を使いたい)

これらの課題を解決するためにどうすればよいのか考えてみたのですが、、、

考えた結果、「Lambda先生にご登場願うしかない」という結論になりました。

「え~? Lambda? コード書きたくないよ!」とおっしゃるそこのアナタ、まあちょっと待ってください。
まずは実現方法を説明しましょう。

Lamdba関数を作成する

まず、日付を取得するシンプルなLambda関数を作成します。

今回は、「実行日の前日の日付」を「JST」で返す関数とします。

import json
from datetime import datetime
from datetime import timedelta
from zoneinfo import ZoneInfo

def lambda_handler(event, context):
    # 現在の日付・時刻を取得する
    dt_now = datetime.now(ZoneInfo('Asia/Tokyo'))

    # 現在に対する「前日の日付」を取得する (YYYY-MM-DD)
    yesterday_date  = (dt_now - timedelta(days=1)).strftime('%Y-%m-%d')

    return {
        'Date': yesterday_date
    }

このLambda関数を実行すると、以下のように「昨日の日付」が戻り値として返ってきます。

{
  "Date": "2022-11-30"
}

ステートマシンを作成する

今回作成するステートマシンは下図のような構成になります。

「Lambda: Invoke」ステート

「設定」タブで、作成したLambda関数を指定します。

「Pass」ステート

このPassステートでは、Lambda関数の戻り値を使って、この後に実行するAPI呼び出しのパラメーターで指定する値を生成します。

「入力」タブを下図のように設定します。

画面キャプチャーが見切れているので、設定内容を以下に記載しておきます。

{
  "StartTime.$": "States.Format('{}T00:00:00.000+09:00', $.Date)",
  "EndTime.$": "States.Format('{}T23:59:59.999+09:00', $.Date)",
  "ObjectKey.$": "States.Format('result_{}.json', $.Date)"
}

組み込み関数Status.Formatを使って、各パラメーターの文字列を生成しています。

「CloudWatch: DescribeAlarmHistory」のパラメーター:

  • StartTime: 取得した日付 (実行日の前日) の開始時刻
  • EndTime: 取得した日付 (実行日の前日) の終了時刻

「S3: PutObject」のパラメーター:

  • ObjectKey: オブジェクトキー名の文字列「result_<日付>.json」

「CloudWatch: DescribeAlarmHistory」ステート

「設定」タブでAPIのパラメーターを指定します。

方式1の時はパラメーターとして「HistoryItemType」のみ指定していましたが、ここでは加えて「StartDate」「EndDate」も指定しています。

このようにパラメーターを指定することで、「実行日の前日1日分のアラーム履歴を出力」という動作を行わせることができます。

「出力」タブは、方式1と同様に「ResultPath」を設定してAPI実行結果を出力に追加するようにします。

「S3: PutObject」ステート

パラメーターは方式1とほぼ同じですが、「ObjectKey」は前のPassステートで用意した値を参照するようにします。

これで、ステートマシンの設定は終わりです。

動作確認

作成したステートマシンを実行して、CloudWatchアラームのアクション実行履歴がS3バケットに格納されることを確認しましょう。

  • EventBridge Schedulerから呼び出す
  • 手動でステートマシンを実行する

いずれの実行方法でも、正しく日付を取得して処理が行われることが確認できるかと思います。

おわりに

今回は、Step Functionsのステートマシンで「日付」を取り扱いたい場合の、2つの方法について紹介しました。

方式1では、コードを全く書かずに「日付」を取得することができます。

「日本時間ではなくUTC」「実行日の日付のみ」「EventBridge Schedulerから呼び出す場合しか使えない」など制限事項は多いですが、「S3オブジェクトに一意なキー名を付与したい」などの目的では利用できるのではないかと思います。

一方、方式2はLambda関数を使用する必要があります。

Lambda関数のコードを記述するということは、「コードのバグ対応」「ランタイムのアップデート対応」など運用・保守の面で対応が必要になるということです。

ただ、今回使用しているような極シンプルなコード内容であれば、テストを十分に行ってコードにバグが無いことをチェックするのは難しくないと思いますし、ランタイムをアップデートしなければならない場合でも非互換はまず発生しないのではないかと思います。
(完全に無いことを保証するものではありません)

このように、必要最小限のLambda関数を組み合わせることで、Step Functionsで行えることの幅が広がりますので、皆さんも上手く活用してみてください。