StepFunctionsだけでS3のPrefix配下を一括コピーする
概要
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の事前に加工できます。
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 公式
-
完全なprefixをparameterで渡せるようであればS3:ListObjectsV2はいりませんが、可変部分のみ渡す場合、Mapの項目ソースのPrefixの指定でAmazon States Languageの関数が使えなかったので上記構成でMapにJsonPayloadでParameterを渡しています。 ↩︎
Discussion