FSx データを S3 に System Manager Run Command でバックアップする

FSx は標準の機能でバックアップを同リージョン内の S3 に作成でき、マネージメントコンソールでバックアップからリストアも簡単にできます。
ですが、現在(2019/11)、別のリージョンに保存する機能が提供されていないため、実現するには aws cli や robocopy などで保存するしかありません。

FSx のウイルスチェック用に起動している Windows Server 2016 があるので、System Manager の Run Command を Cloud Watch Event でスケジュールし日次で実行することにしました。

## AWS System Manager ドキュメント

Run Command で実行するにはドキュメントに JSON か YAML でスクリプトを記述する必要があります。サンプルは、YAML で記述しています。スクリプトは powershell です。

スクリプトの特徴としては、データをコピーするためにタスクスケジューラにタスクを作成し、タスク内のコマンドでコピーしていることです。理由は、AD の Admin で実行するためです。ファイルアクセスを Windows ACL で制御しているような場合(殆どの企業では設定されていると思いますが)、Run Command の実行ユーザーでは必ず権限のエラーが発生します。[^1]

ドメインのユーザーで実行するにはタスクの実行ユーザーを登録して実行する必要があります。

```yaml

---
schemaVersion: "2.2"
description: "Command Document FSx data copy to another region."
parameters:
   Src:
     type: "String"
     description: "source path."
     default: "\\\\domain.local\\share"
   Dest:
     type: "String"
     description: "target buket."
     default: "s3://data-backup/backup"
   executionTimeout:
     type: "String"
     description: "タイムアウト時間(単位:秒)。最大値 172800 (48 hours)"
     default: "86400"
mainSteps:
- action: "aws:runPowerShellScript"
   name: "copyToAnotherRegion"
   inputs:
     timeoutSeconds: "{{executionTimeout}}"
     runCommand:
       - "$user = (aws ssm get-parameters --names domain-user-nm --with-decryption --region ap-northeast-1 --query Parameters[].Value --output text)"
       - "$pass = (aws ssm get-parameters --names domain-user-pwd --with-decryption --region ap-northeast-1 --query Parameters[].Value --output text)"
       - "$task_name = 'fsxCopyToS3'"
       - "$command = 'PowerShell.exe -Command aws s3 sync --exact-timestamps --delete {{Src}} {{Dest}}'"

       - "function isExistSchedule($tn) { return (@(Get-ScheduledTask | where TaskName -eq $tn).Count -gt 0); }"
       - "function isRunning($tn) { if (isExistSchedule $tn) { return (Get-ScheduledTask $task_name).State -eq 'Running'; } else { return $False; } }"
       - "function deleteSchedule($tn) { if (isExistSchedule $tn) { schtasks /Delete /tn $tn /F; } }"

       # 処理中の場合正常終了します。
       - "if ((isExistSchedule $task_name) && (isRunning $task_name)) { exit 0; } }"

       - "deleteSchedule $task_name"
       - "schtasks /create /TN $task_name /RU $user /RP $pass /F /TR $command /SC once /ST 23:59:59"
       - "schtasks /run /tn $task_name"

       # 処理が終わるまで待ちます。
       - "while (isRunning $task_name) { Start-Sleep 1; }"

       - "$result = (Get-ScheduledTaskInfo $task_name).LastTaskResult"

       # 異常終了時はログを確認したいので、正常終了したときのみタスクを削除します。
       - "if ($result -eq 0) { deleteSchedule $task_name; }"

       - "exit $result"
```

### 解説

```yaml
   Src:
     type: "String"
     description: "source path."
     default: "\\\\domain.local\\share"
```
Windows のセパレーターの"￥"を yaml 内に書く場合は、エスケープする必要があります。[^2]

```yaml
timeoutSeconds: "{{executionTimeout}}"
```
処理時間がかかる場合は timeoutSeconds に値をセットします。デフォルト値は不明です。

```powershell
$user = (aws ssm get-parameters --names domain-user-nm --with-decryption --region ap-northeast-1 --query Parameters[].Value --output text)
$pass = (aws ssm get-parameters --names domain-user-pwd --with-decryption --region ap-northeast-1 --query Parameters[].Value --output text)
```
ドメインの管理者ユーザー名とパスワードをソース内に書くのはセキュリティー的に問題があります。
System Mnager のパラメータストアから取得するようにしています。

```powershell
$command = 'PowerShell.exe -Command aws s3 sync --exact-timestamps --delete {{Src}} {{Dest}}'
```
s3 sync でコピーします。パラメータには、変数名を2重のブレースで囲って取得します。

```powershell
function isExistSchedule($tn) { return (@(Get-ScheduledTask | where TaskName -eq $tn).Count -gt 0); }
function isRunning($tn) { if (isExistSchedule $tn) { return (Get-ScheduledTask $task_name).State -eq 'Running'; } else { return $False; } }
function deleteSchedule($tn) { if (isExistSchedule $tn) { schtasks /Delete /tn $tn /F; } }
```
それぞれ、タスクの存在チェック、実行中チェック、タスク削除の関数です。yaml で複数行に書く方法がわからなかったので、1行で書いています(わかる方いたら教えて下さい)。

```powershell
schtasks /create /TN $task_name /RU $user /RP $pass /F /TR $command /SC once /ST 23:59:59
schtasks /run /tn $task_name
```
タスクを登録して、実行しています。
未来時間にしないとワーニングで Run Command が終了してしまうので実行時間を "/ST 23:59:59" にしています。それ以外、特に特別なことはしていません。

```powershell
while (isRunning $task_name) { Start-Sleep 1; }
```
タスク実行後、シェルスクリプトは処理を待たずに進んでしまうのでタスクの終了を待ちます。


[^1]: Run Command は Linux 系なら root、Windows なら Administrator で実行されます。
[^2]: 但し、マネージメントコンソールでパラメータを渡すう場合(Run Command や Cloud Watchなど)は、エスケープする必要はありません。

[ssm_doc]:https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/ssm-agent.html