10
7

More than 5 years have passed since last update.

S3へコピーしたデータが、本当にコピー元と同一かどうか検証してみた時のお話

Posted at

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計算は、以下のコードを参考にしました。(計算ロジックに若干の修正をしています。)

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.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
}
10
7
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7