Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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
}
speaktech
AWSが好きなインフラ屋さんです。 検証結果や作ったものを紹介していきます。 LGTMをいただけると、励みになります。記事内容にご意見・ご質問等があれば、全て回答しますのでコメントをお寄せください!
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした