1. はじめに
昨年末、自宅のPCに古い細々したデータがたまってきたので最終更新日時が一定期間経過したデータをAWSのクラウドスレージサービスS3へアーカイブするスクリプトを PowerShell で書いていました。
単純にコピーするだけであれば、AWSで提供している「AWS Tools for Windows PowerShell」の Write-S3Objectコマンドを使えば簡単に実行できます。
ただし、S3にコピーしたからといって、ローカルのデータは消すには勇気がいります。**「本当にコピー元と先で同一なんだよね?」**って若干不安になりました。
そこで、単にS3へコピーしてローカルのデータを削除してしまうのではなく、コピーしたらコピー元と先で同一であるかを検証する処理を加えることにしました。
検証するに当たり、S3の仕様について色々と気づきがありましたので、その内容を共有いたします。
2. 必要な準備
2.1. AWS Tools for Windows PowerShell のインストール
以下のページからAWS Toolsをダウンロードして、インストールします。
https://aws.amazon.com/jp/powershell/
詳細なセットアップ手順や前提条件は以下のページに記載されています。
https://docs.aws.amazon.com/ja_jp/powershell/latest/userguide/pstools-getting-set-up-windows.html#prerequisites
2.2. プロファイルの作成
上記のAWS Toolsをインストールしたら、AWSアカウントのアクセスキーとシークレットキーを Set-AWSCredential コマンドで登録し、プロファイルを作成します。
https://docs.aws.amazon.com/ja_jp/powershell/latest/userguide/specifying-your-aws-credentials.html
# 例1)プロファイルの登録
PS C:\> Set-AWSCredential -AccessKey アクセスキー -SecretKey シークレットキー -StoreAs プロファイル名
これで、必要な準備は完了です。
3. S3の仕様
3.1. マルチパート・アップロード機能について
PowerShellでS3へアップロードする場合、Write-S3Object コマンドを利用します。
Write-S3Object によるアップロードは、16MB以上のデータが対象となる場合強制的にマルチパート・アップロードが実行されます。
マルチパート・アップロードは、ローカルデータを5MBのブロックサイズに区切り、S3へ並列でアップロードする処理を指します。
デフォルトでは、10パート並列でアップロードが実行されます。並列での同時アップロード数は -ConcurrentServiceRequest サブコマンドで制御することもできるので、簡易的な帯域制御が可能です。
( netstatコマンドで確認したところ、確かに10セッションがS3に対して確立されていました。)
3.2. S3オブジェクトのeTagプロパティについて
S3へアップロードしたデータの完全性(コピー元と先で同一であるか)を検証するため、調査していたら、S3のeTagプロパティがローカルデータのMD5ダイジェストと一致することに気づきました。
(公式にもMD5ダイジェストを利用している記述がありました。)
これを比較すれば、ローカルデータがS3上に完全な状態でコピーされたことを確認できると思ったのですが、、マルチパート・アップロードが行われる際はうまく適用できませんでした。
というのも、S3のeTagはマルチパート・アップロードの有無によって、以下のロジックを使い分けて、eTagを計算しているからです。
i. マルチパート・アップロード無(16MBよりも小さいデータ)のeTag
ローカルデータのMD5ダイジェストと一致
例)10MBのデータ
S3のeTagプロパティ:cd573cfaace07e7949bc0c46028904ff
ローカルデータのMD5:cd573cfaace07e7949bc0c46028904ff
ii. マルチパート・アップロード有(16MB以上のデータ)のeTag
①データをバイナリ形式で読み取る
②パート毎に区切り、それぞれのMD5ダイジェストを取得する
※S3のパートサイズは5MB
③パート毎のMD5ダイジェストを結合する
④結合したMD5ダイジェストのMD5ダイジェストを取得する
⑤マルチパートの総パート数をハイフン付きで④の末尾に付加する
例)1GBのデータ
S3のeTagプロパティ:cb45770d6cf51effdfb2ea35322459c3-205
4. 完全性の検証
4.1. 検証の方針
ファイルサイズ(16MBが閾値)に応じて、S3のeTagの値をローカルデータから計算し、実際のeTagプロパティと比較することにしました。
マルチパート・アップロード有(16MB以上のデータ)のeTag計算は、以下のコードを参考にしました。(計算ロジックに若干の修正をしています。)
- Calculate Amazon eTag hash for named file. Powershell
https://gist.github.com/seanbamforth/9388507
4.2. 検証の結果
fsutil コマンドで作成した15MB、16MB、17MBのダミーファイルや大容量ファイルで検証してみましたが、いずれもローカルデータから計算したS3のeTag値と実際のeTagプロパティが一致していることを確認できました。
以上をもって、「コピー元と先で同一であること」を確認する手順を確立したので、この手順で完全性を確認できた際は、安心してローカルのデータを削除できます。
5. あとがき
検証に利用したPowershellコマンド群を「S3のeTag値をローカルデータから計算する関数 Get-S3ETagHash」と「完全性を確認してローカルデータを削除するという一連の処理をする関数 Archive-FileToS3」としてスクリプトにまとめました。
(前者は、https://gist.github.com/seanbamforth/9388507 を参考にしています)
このスクリプト Archive-FileToS3.ps1 をインポートすることで、以下のように関数を利用できます。参考までに下に掲載しておきます。
# 例2)スクリプトのインポートと関数の利用方法
PS D:\Tmp> Import-Module .\Archive-FileToS3.ps1
PS D:\Tmp> Get-S3ETagHash -Path D:\Tmp\16MB.txt
Algorithm Hash Path
--------- ---- ----
S3ETag eafa449afe224ad0b7f8f5bab4145d13-4 D:\Tmp\16MB.txt
PS D:\Tmp> Archive-FileToS3 -Path アーカイブ対象フォルダ -Bucketname バケット名 -Days 期間
[参考]Powershell スクリプト(Archive-FileToS3.ps1)
# Archive-FileToS3 archives local files which is older than specified days in a path to S3bucket with verifying integrity of uploaded S3objects.
# The verification method is to check if an eTag of S3object matches an eTag value calculated from a local file.
# Get-S3ETagHash calculates an eTag for a local file that should match the S3 eTag of the uploaded file.
# Credit goes to Sean Bamforth (https://gist.github.com/seanbamforth/9388507) and chrisdarth(https://gist.github.com/chrisdarth/02d030b31727d70d2c63)
function Archive-FileToS3 {
[cmdletbinding()]
Param (
[Parameter(Mandatory=$true)]
[ValidateScript({ Test-Path $_ -PathType Container })]
[string]$Path,
[Parameter(Mandatory=$true)]
[ValidateScript({ $( Get-S3Bucket -BucketName $_ ) })]
[string]$Bucketname,
[Parameter(Mandatory=$true)]
[Int32]$Days,
[Int32]$ConcurrentServiceRequest = 10
)
if ($Path[$Path.Length-1] -eq "\" ){ $Path = $Path.Substring(0,$Path.Length-1) }
foreach ($file in $(Get-ChildItem $Path -Recurse | Where-Object{$_.Attributes -ne "directory"})){
if ($file.LastWriteTime -lt ((Get-Date).AddDays(-$Days))){
Write-S3Object -BucketName $Bucketname -Key $file.FullName.Substring($Path.Length + 1) -File $file.FullName -ConcurrentServiceRequest $ConcurrentServiceRequest
$s3object = Get-S3Object -BucketName $Bucketname -Key $file.FullName.Substring($Path.Length + 1)
$etag = $s3object.etag.Replace("`"","")
$hash = $(Get-S3ETagHash($file.FullName)).Hash
if ($etag -eq $hash){
Remove-Item $file.FullName -Force
}else{
Remove-S3Object -BucketName $Bucketname -Key $file.FullName.Substring($Path.Length + 1) -Force
}
}
}
}
function Get-S3ETagHash {
[cmdletbinding()]
Param (
[Parameter(Mandatory=$true)]
[ValidateScript({ Test-Path $_ -PathType Leaf })]
[string]$Path,
[Int32]$ChunkSize = 5
)
$filename = Get-Item $Path
$md5 = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider
$blocksize = (1024*1024*$ChunkSize)
$startblocks = (1024*1024*16)
$lines = 0
[byte[]] $binHash = @()
$reader = [System.IO.File]::Open($filename,"OPEN","READ")
if ($filename.length -ge $startblocks) {
$buf = new-object byte[] $blocksize
while (($read_len = $reader.Read($buf,0,$buf.length)) -ne 0){
$lines += 1
$binHash += $md5.ComputeHash($buf,0,$read_len)
}
$binHash=$md5.ComputeHash( $binHash )
}
else {
$lines = 1
$binHash += $md5.ComputeHash($reader)
}
$reader.Close()
$hash = [System.BitConverter]::ToString( $binHash )
$hash = $hash.Replace("-","").ToLower()
if ($lines -gt 1) {
$hash = $hash + "-$lines"
}
# Output pscustomobject, equal to Get-FileHash
[pscustomobject]@{
Algorithm = "S3ETag"
Hash =$hash
Path = $filename.FullName
}
return
}