🐖

StepFunctionsだけでS3のPrefix配下を一括コピーする

2023/03/06に公開

概要

aws s3 cp s3:/hoge/xxxx s3:/huga/yyyyMMdd/xxxx --recursive みたいなことをStepFunctionsだけでやろうとしたらそこそこめんどくさかったので、メモ書き程度にのこします。

やりたいこと

タイトル通り。

Lambdaを使わずに、アクション(フロー)とAmazon States Languageだけで一括コピーする。

さきに結論

ある程度型にはまったことをやるならコーディングしなくてすみますが、細かい要件があるならおとなしくLambda Invoke使ったほうが楽です。

日付の操作など、いつも当たり前にやってることがびっくりするぐらい冗長なコードになってしまいました。

ノーコードで細かいことしようとするとラーニングコスト高いなあというお気持ちになりました。

ワークフロー全体像

こういう感じになりました

使ったアクションとフローは以下の3種類。

  • S3:ListObjectsV2
  • Map
  • S3:CopyObject

一個のアクションではできなかったため、リスト取得→マップでループ→コピーという流れで実現しています。[1]

最終的なコードは以下になりました。

{
  "Comment": "A description of my state machine",
  "StartAt": "ListObjectsV2",
  "States": {
    "ListObjectsV2": {
      "Type": "Task",
      "Parameters": {
        "Bucket": "hogehoge_bucket",
        "Prefix.$": "States.Format('hogehoge_prefix/{}', $.NewVersion)"
      },
      "Resource": "arn:aws:states:::aws-sdk:s3:listObjectsV2",
      "Next": "S3 object keys"
    },
    "S3 object keys": {
      "Type": "Map",
      "ItemProcessor": {
        "ProcessorConfig": {
          "Mode": "DISTRIBUTED",
          "ExecutionType": "STANDARD"
        },
        "StartAt": "CopyObject",
        "States": {
          "CopyObject": {
            "Type": "Task",
            "Parameters": {
              "Bucket": "hugahuga_bucket",
              "CopySource.$": "States.Format('hogehoge_bucket/{}', $.Key)",
              "Key.$": "States.Format('copy/{}{}{}/{}', $.Year,$.Month,$.Day, $.FileName)"
            },
            "Resource": "arn:aws:states:::aws-sdk:s3:copyObject",
            "End": true
          }
        }
      },
      "MaxConcurrency": 1000,
      "Label": "S3objectkeys",
      "End": true,
      "ItemsPath": "$.Contents",
      "ItemSelector": {
        "FileName.$": "States.ArrayGetItem(States.StringSplit($$.Map.Item.Value.Key, '/'), States.MathAdd(States.ArrayLength(States.StringSplit($$.Map.Item.Value.Key, '/')), -1))",
        "Key.$": "$$.Map.Item.Value.Key",
        "Year.$": "States.ArrayGetItem(States.StringSplit(States.ArrayGetItem(States.StringSplit($$.State.EnteredTime, 'T'),0),'-'),0)",
        "Month.$": "States.ArrayGetItem(States.StringSplit(States.ArrayGetItem(States.StringSplit($$.State.EnteredTime, 'T'),0),'-'),1)",
        "Day.$": "States.ArrayGetItem(States.StringSplit(States.ArrayGetItem(States.StringSplit($$.State.EnteredTime, 'T'),0),'-'),2)"
      }
    }
  }
}

Tips

S3:ListObjectsV2でのKeyの取得からMapへの引き渡し

S3:ListObjectsV2では以下のような形でパラメータを指定し、Objectsを取得します。

{
  "Bucket": "hogehoge-bucket",
  "Prefix.$": "States.Format('hogehoge-prefix/{}', $.NewVersion)"
}

s3のhogehoge-bucketバケットの、hogehoge-prefixの配下にyyyy-MM-dd形式の日付でバージョンを切り、ファイルを保存していく想定とします。

StepFunctionsの起動時にParameterにNewVersionを渡し、hogehoge-prefix/yyyy-MM-dd/の配下にあるObjectsのキーをすべて取得します。

固定値であるhogehoge-prefixはAmazon States LanguageのStates.Format関数を使用して補完します。

レスポンスは以下の様に返ってきます。

{
  "Contents": [
    {
      "ETag": "\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"",
      "Key": "hogehoge-prefix/2023-02-28/hogehoge-0-0.parquet",
      "LastModified": "2023-02-28T01:02:56Z",
      "Size": 8744803,
      "StorageClass": "STANDARD"
    },
    {
      "ETag": "\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"",
      "Key": "hogehoge-prefix/2023-02-28/hogehoge-0-1.parquet",
      "LastModified": "2023-02-28T01:02:56Z",
      "Size": 5096990,
      "StorageClass": "STANDARD"
    }
  ],
  "IsTruncated": false,
  "KeyCount": 2,
  "MaxKeys": 1000,
  "Name": "hogehoge-bucket",
  "Prefix": "hogehoge-prefix/2023-02-28"
}

Contents配下が各Objectsの情報のため、Mapの項目配列へのパスを指定で、$.Contentsを指定します。

MapからCopyObjectsに渡す

特に何も指定しなければ、Contents配列の値が、Map処理内のS3:CopyObjectにinputとして渡ります。

{
  "ETag": "\"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"",
  "Key": "hogehoge-prefix/2023-02-28/hogehoge-0-0.parquet",
  "LastModified": "2023-02-28T01:02:56Z",
  "Size": 8744803,
  "StorageClass": "STANDARD"
}

inputの値を加工したい場合は[ItemSelector]で項目を変更に入力することでinputの事前に加工できます。

.Map.Item.Valueで配列の1要素の指定が可能です。

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/input-output-contextobject.html#contextobject-map

CopyObjectsで加工した値を引数します。


{
  "Bucket": "hugahuga-bucket",
  "CopySource.$": "States.Format('hogehoge-bucket/{}', $.Key)",
  "Key.$": "States.Format('copy/{}{}{}/{}', $.Year,$.Month,$.Day, $.FileName)"
}

そのまま渡しても値は作れますが、同一の関数を2、3度指定する必要がありごちゃごちゃしたので、ItemSelectorで事前に加工し、CopyObjectsでの指定を綺麗に書けるようにしました。

多用した関数

パスや日付の加工のために以下の関数を多用しました。

  • States.ArrayGetItem 配列のindexを指定して値を取得する。Keyの末尾の名称取得等に使用。
  • States.Format 文字列をフォーマットする。パスへの変数埋め込みに使用。
  • States.StringSplit 文字列をdemiliterでsplitする。Keyや日付の分解に使用。

調べればもっとスマートにできる方法があるかも知れませんが、今回は上記を駆使して実装しました。

日付の取り扱いについて

Amazon States Languageには日付で気が利くFormatができる関数はありません。

StateMachineの開始日は$$.State.EnteredTimeで取得できるため、
処理日などをPrefixにコピー先のPrefixに指定したい場合等、こちらを使うことになります。

Fomat関数や正規表現が使えない中、Json指定で複数の関数を駆使して加工することになるため、
yyyy-MM-ddTHH:MM:SSZ形式からなるべく離さない形でPrefixの設計ができると、
実装が楽になります。

yyyyMMddをルールにした場合、Tでsplitして日付を抽出→-でsplitして年、月、日を分解→formatで結合というめんどくさい流れになりました。

参考

AWS 公式

https://states-language.net/spec.html

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/input-output-contextobject.html#contextobject-map

脚注
  1. 完全なprefixをparameterで渡せるようであればS3:ListObjectsV2はいりませんが、可変部分のみ渡す場合、Mapの項目ソースのPrefixの指定でAmazon States Languageの関数が使えなかったので上記構成でMapにJsonPayloadでParameterを渡しています。 ↩︎

Discussion