PowershellでS3にフォルダを移動するスクリプトを作った

# 前提
S3 や SNS についての説明や AWS Tools for Windows PowerShell のインストール手順などは本稿では取り扱いません。

# 概要図
![BLOG-S3-PowerShell.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/b5564f1a-0b38-2ccf-bdea-bd3eb7cca5ad.png)

実行ログ保管に CloudWatch Logs も使った方が良さそうですが、簡単なスクリプトなので下記の点から、SNS のみとしています。

- 単一障害点を無くすため
- 構成をシンプルにするため
- 成功通知が来ない事で実行の失敗や予期しない問題に気づけるようにするため

# 処理概要
タスクスケジューラなどで定期実行される事を想定しています。

1. カレントディレクトリにスクリプトの実行ログを出力する。
1. 前日までのスクリプト実行ログファイルを S3 へアップロードする。
1. スクリプト実行ログのフォルダから前日までのログファイルを削除する。
1. 移動対象のフォルダを S3 へアップロードする。
1. アップロードしたフォルダ内のファイルを削除する。
1. SNS で実行結果を通知する。

## 移動イメージ
実行マシン上のディレクトリ構成は一例です。

- 対象フォルダ配下のフォルダ/ファイルも同じ構成でアップロードします。
- ルートディレクトリから対象フォルダまでの階層が深い場合、親の階層構造は維持したままアップロードします。
- 指定していないフォルダのファイルや、対象フォルダより上の階層のファイルはアップロードしません。

```tree:実行マシン
C:\example\ROOT（指定するルートディレクトリ）
│
│ app.txt
│
├─config
│      buzz.txt
│      fizz.txt
│
├─data（指定する対象フォルダ）
│      hoge.txt
│      huga.txt
│
├─export（指定する対象フォルダ）
│   │  foo.txt
│   │  bar.txt
│   │
│   └─file
│          baz.txt
│
└─Documents
    │  aaa.txt
    │  bbb.txt
    │
    ├─data（指定する対象フォルダ）
    │      ccc.txt
    │      ddd.txt
    │
    └─file
           eee.txt
           fff.txt
```

```tree:S3 バケット
powershell-move-123456789012
/backup
│
├─log
│      move2S3-20230417.log
│      move2S3-20230418.log
│
├─data（指定した対象フォルダ）
│      hoge.txt
│      huga.txt
│
├─export（指定した対象フォルダ）
│   │  foo.txt
│   │  bar.txt
│   │
│   └─file
│          baz.txt
│
└─Documents
    │
    └─data（指定した対象フォルダ）
           ccc.txt
           ddd.txt
```

# AWS Tools for Windows PowerShellについて
PowerShell コマンドラインから AWS リソースに対する操作が行えるツールです。
PowerShell 版 AWS CLI のようなものです。

ローカルのWindowsやLinux、MacOSにもインストールできます。

https://aws.amazon.com/jp/powershell/

AWS 公式 AMI の Windows には AWS Tools for Windows PowerShell がプリインストールされています。
>Tools for Windows PowerShell (AWSPowerShell モジュール) は、すべての Windows ベースの Amazon Machine Image (AMI) にデフォルトでインストールされます。

https://docs.aws.amazon.com/powershell/latest/userguide/pstools-getting-set-up-windows.html

# 環境情報
本稿ではツールをインストールする代わりに、以下の AMI で EC2 を起動しています。

AMI ID：`ami-0b168f9cd578fe5d5`
AMI 名：`Windows_Server-2022-English-Full-Base-2023.03.15`

```powershell
PS > Get-ComputerInfo

WindowsBuildLabEx                                       : 20348.1.amd64fre.fe_release.210507-1500
WindowsCurrentVersion                                   : 6.3
WindowsEditionId                                        : ServerDatacenter
WindowsProductName                                      : Windows Server 2022 Datacenter
WindowsVersion                                          : 2009
OSDisplayVersion                                        : 21H2


PS > $PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.1.20348.1366
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.20348.1366
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1


PS > Get-AWSPowerShellVersion

AWS Tools for Windows PowerShell
Version 4.1.285
Copyright 2012-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.

Amazon Web Services SDK for .NET
Core Runtime Version 3.7.105.13
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

Release notes: https://github.com/aws/aws-tools-for-powershell/blob/master/CHANGELOG.md

This software includes third party software subject to the following copyrights:
- Logging from log4net, Apache License
[http://logging.apache.org/log4net/license.html]
```

# リソース作成
移動先 S3 バケットと、スクリプト実行結果通知先 SNS トピックを作成します。
必要に応じて後項の IAM 権限設定も適宜実施してください。

## S3
名前は指定して、その他はデフォルト設定で大丈夫です。
![2023-04-18_16h52_19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/6f19ab22-81db-5309-211c-99733a523522.png)

## SNS
名前と、タイプはスタンダードを指定して、その他はデフォルト設定で大丈夫です。
![2023-04-18_16h54_46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/ba069674-ee36-d45f-e04d-263ab1ca7861.png)

サブスクリプションも作成します。
![2023-04-18_17h02_37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/ef6306bf-0f89-c851-ad55-e8a3f1c14dbc.png)

# 権限設定
本稿では下記の IAM ポリシーをアタッチした IAM ロールを、 EC2 にアタッチしています。

スクリプトを実行する上で必要最小限の権限しか許可していません。
S3 バケット名や SNS トピック ARN 指定箇所は作成したリソースの値で置き換えてください。

```json:iampolicy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:PutObject",
            "Resource": [
                "arn:aws:s3:::powershell-move-123456789012",
                "arn:aws:s3:::powershell-move-123456789012/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "sns:Publish",
            "Resource": "arn:aws:sns:ap-northeast-1:123456789012:powershell-move"
        }
    ]
}
```

# スクリプト
AWS Tools for PowerShell には、AWS CLI の`s3 mv`（あるいは Linux の`mv`コマンド）相当のコマンドレットが無い為、mv のような動作をするようにスクリプトを作成しました。
※引数などには対応していません。

`$ErrorActionPreference = 'Stop'`設定について
Write-S3Object コマンドレットはエラー発生時に終了するエラーとなるので catch できますが、フォルダ検索や削除処理、Publish-SNSMessage コマンドレットは終了しないエラーで処理が継続するため catch できず、正常に動作していないのに成功した事になってしまうため設定しています。
エラーハンドリングが雑ですが、処理一つ一つに`-ErrorAction Stop`を付けたり`$?`で分岐するのは手間だったので、全体に適用されるよう指定しています。

```powershell:move2S3.ps1
$ROOT_DIRECTORY = "C:\example\ROOT"
$MOVE_TARGET_FOLDER_NAME = "data", "export" # カンマ区切りで複数指定可能
$S3_BUCKET_NAME = "powershell-move-123456789012"
$DESTINATION_PATH = "backup"
$SNS_TOPIC_ARN = "arn:aws:sns:ap-northeast-1:123456789012:powershell-move"
$LOG_FOLDER_NAME = "log"
$LOGFILE_NAME = "move2S3-" + (Get-date -Format "yyyyMMdd") + ".log"

function Write-Log ($message) {
    if (!(Test-Path $LOG_FOLDER_NAME)) {
        New-Item $LOG_FOLDER_NAME -Type Directory
    }
    $txt = (Get-date -Format "yyyy/MM/dd HH:mm:ss") + " " + $message
    Write-Output $txt | Out-File -FilePath "$LOG_FOLDER_NAME\$LOGFILE_NAME" -Append
    return $null
}

function Set-Error ($error) {
    Write-Log "エラー発生"
    $error[0] >> "$LOG_FOLDER_NAME\$LOGFILE_NAME"
    Write-Log ("Publish-SNSMessage -TopicArn $SNS_TOPIC_ARN -Subject 【失敗】S3への移動スクリプト実行中にエラーが発生しました -Message (失敗) " + [Net.Dns]::GetHostName() + " にて S3への移動スクリプト の実行中にエラーが発生しました")
    Publish-SNSMessage -TopicArn $SNS_TOPIC_ARN -Subject "【失敗】S3への移動スクリプト実行中にエラーが発生しました" -Message ("(失敗) " + [Net.Dns]::GetHostName() + " にて S3への移動スクリプト の実行中にエラーが発生しました") | ForEach-Object {Write-Log $_}
    exit 1
}

try {
    $ErrorActionPreference = 'Stop'
    Write-Log "処理開始"

    Write-Log "前日までの実行ログ移動"
    Write-Log "Write-S3Object -BucketName $S3_BUCKET_NAME -KeyPrefix $DESTINATION_PATH/$LOG_FOLDER_NAME -Folder $LOG_FOLDER_NAME -Recurse"
    Write-S3Object -BucketName $S3_BUCKET_NAME -KeyPrefix "$DESTINATION_PATH/$LOG_FOLDER_NAME" -Folder $LOG_FOLDER_NAME -Recurse
    Write-Log "Remove-Item $LOG_FOLDER_NAME -Exclude $LOGFILE_NAME -Recurse"
    Remove-Item $LOG_FOLDER_NAME -Exclude $LOGFILE_NAME -Recurse

    Write-Log "移動対象ファイル一覧"
    $target_folder_path_array = @()
    foreach ($folder_name in $MOVE_TARGET_FOLDER_NAME) {
        $target_folder_path = (Get-ChildItem -Path $ROOT_DIRECTORY -Filter $folder_name -Attributes D -Recurse).FullName
        $target_folder_path_array += $target_folder_path
        (Get-ChildItem $target_folder_path -File).Name >> "$LOG_FOLDER_NAME\$LOGFILE_NAME"
    }

    Write-Log "対象フォルダ移動"
    foreach ($target_folder_path in $target_folder_path_array) {
        $key_path =  $target_folder_path.Replace($ROOT_DIRECTORY, "").Replace("\", "/")
        Write-Log "Write-S3Object -BucketName $S3_BUCKET_NAME -KeyPrefix $DESTINATION_PATH$key_path -Folder $target_folder_path -Recurse"
        Write-S3Object -BucketName $S3_BUCKET_NAME -KeyPrefix $DESTINATION_PATH$key_path -Folder $target_folder_path -Recurse

        Write-Log "Remove-Item -Path ${target_folder_path}\* -Recurse"
        Remove-Item -Path "${target_folder_path}\*" -Recurse
    }

    Write-Log "Publish-SNSMessage -TopicArn $SNS_TOPIC_ARN -Message (成功) S3への移動処理が完了しました"
    Publish-SNSMessage -TopicArn $SNS_TOPIC_ARN -Message "(成功) S3への移動処理が完了しました" | ForEach-Object {Write-Log $_}
    exit 0
} catch {
    Set-Error $error
} finally {
    Write-Log "処理終了"
    $ErrorActionPreference = 'Continue'
}
```

S3 には 秒間 3,500 件の Put リクエストが可能ですが、スクリプトはこの制約を超えないという想定のもと、作成しています。
制約を超えて、エラーコード「503 Slow Down」となる場合、ループ処理の中に Sleep を入れる等の処置が必要です。

>S3 バケットのプレフィックスごとに 1 秒あたり 3,500 件の PUT/COPY/POST/DELETE リクエスト、または 5,500 件の GET/HEAD リクエストを送信することができます。

https://repost.aws/ja/knowledge-center/http-5xx-errors-s3

# 実行結果
成功時メール
![2023-04-18_16h46_34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/80ce60bf-af33-c631-df7e-e2e0e81d5395.png)

失敗時メール
![2023-04-18_16h48_36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/f3f46b35-01f1-cd57-8f8c-1bfcba0c4d26.png)

S3 バケット上の格納イメージ
![2023-04-18_17h08_45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/b74a60d8-7d6a-6172-5fcf-6af5ef96b338.png)
![2023-04-18_17h22_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/b1eefbb8-8c02-f441-c8ad-0dd4ad6811c7.png)
![2023-04-18_17h23_54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269716/30524f4e-c77f-1ae9-0910-5c9a7494d18b.png)

t3a.large のインスタンスタイプで 1440 ファイル、1 ファイルあたり 1.5MB（合計ファイルサイズは2.1GB）を対象として計測したところ、5分程度で実行完了しました。

# 参考

## AWS Tools for Windows PowerShell

https://docs.aws.amazon.com/powershell/latest/userguide/pstools-welcome.html

### インストール・セットアップ

https://aws.amazon.com/jp/powershell/

https://docs.aws.amazon.com/powershell/latest/userguide/pstools-getting-set-up.html

https://docs.aws.amazon.com/powershell/latest/userguide/pstools-getting-started.html

### 使用例

https://docs.aws.amazon.com/powershell/latest/userguide/pstools-using.html

https://docs.aws.amazon.com/ja_jp/powershell/latest/userguide/pstools-s3-upload-object.html

### コマンドレットリファレンス

https://docs.aws.amazon.com/powershell/latest/reference/Index.html

#### S3 アップロード

https://docs.aws.amazon.com/powershell/latest/reference/items/Write-S3Object.html

#### SNS 送信

https://docs.aws.amazon.com/powershell/latest/reference/items/Publish-SNSMessage.html

## PowerShell

### ログ出力

https://tooljp.com/windows/chigai/html/Programming/Write_Host-Write_Output_echo-chigai.html

### ファイル削除

https://qiita.com/mima_ita/items/ae31f3a19389e69b307f#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E5%89%8A%E9%99%A4

### パイプ処理

https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_pipelines

### エラーハンドリング

https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-exceptions

https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_try_catch_finally

https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_preference_variables#erroractionpreference

https://devblogs.microsoft.com/scripting/understanding-non-terminating-errors-in-powershell/