Amazon Glacierを使ってデータの長期間保存を始める
Amazon Glacierとは
Amazon Glacierは、1GBのデータを約1円/月で保存することができるアーカイブサービスです。頻繁に取り出すものではない監査ログやバックアップデータ等を入れておく貯蔵庫(ボルト)を用意して中に保存します。Glacierの意味は氷河です。サービス自体は、S3と似ていると思われますが使い勝手が違います。S3はバケットの中にファイル単位で大量のデータを保存し、必要あれば直ぐに取り出すことができます。Glacierはアップロードを高速かつ簡単に行うことができますが、ダウンロードには時間が掛かります。S3と同じく、99.999999999%の堅牢性がありますので、おそらく離れた3箇所以上に同じデータを保存していると思います。テープバックアップを銀行の3支店の金庫にコピーして保存しておくイメージでしょうか。
Glacierの全体像
Glacierの利用にあたり、他のサービスとどのように違うのか1枚の絵で表しました。
Glacierの利用手順
早速、Glacierを使ってみたいと思います。手順は3つです。初めに貯蔵庫となるボルト(Vault)を作成します。次に、このボルトにアーカイブをアップロードします。アップロード後にアーカイブIDが発行されます。最後に、このIDを使ってアーカイブを取り出すジョブを登録します。ジョブが完了した際にAmazon SNSを使った通知を得ることもできますのでアプリケーション間の連携も容易です。ジョブが完了してから24時間はアーカイブを直ぐにダウンロードできますので、通知をトリガーにダウンロードしたり、ポーリングをして確認することができます。
ボルト(Vault)の作成
AWSマネージメントコンソールからGlacierの画面に遷移しボルトの作成をします。東京リージョンを指定することができます。
作成に合わせてAmazon SNSの通知サービスを登録します。ジョブが終わった際にメールで通知が来るようにトピックを作成します。
特に難しい設定はありません。先に進めると完了します。
ついでにSNSの設定をします。トピックに対するサブスクリプションの設定です。これをやっておかないと通知がメールで飛んで来ません。SNSは1回しか通知が来ませんが、転送先としてSQSを指定することで通知内容を溜めておく事ができます。
ボルトの情報を表示する
ボルトにアーカイブをアップロードする方法は今のところプログラムしかありません。そのうちManagementConsole経由のWebフォームが出てくるのではと思います。今回は、Javaからファイルを指定してアップロードしてみたいと思います。
Javaの環境からということで、Eclipseを使います。AWS SDK for Javaをバージョン1.3.18まで上げておいてください。Glacier SDKを使うために必要です。まずは、自分の管理しているボルトの一覧を表示したいと思います。
import java.util.List; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.PropertiesCredentials; import com.amazonaws.services.glacier.AmazonGlacierClient; import com.amazonaws.services.glacier.model.DescribeVaultOutput; import com.amazonaws.services.glacier.model.ListVaultsRequest; import com.amazonaws.services.glacier.model.ListVaultsResult; public class AwsConsoleApp { private static AmazonGlacierClient glacier; private static AWSCredentials credentials; private static void init() throws Exception { credentials = new PropertiesCredentials( AwsConsoleApp.class .getResourceAsStream("AwsCredentials.properties")); glacier = new AmazonGlacierClient(credentials); glacier.setEndpoint("https://glacier.ap-northeast-1.amazonaws.com/"); } public static void main(String[] args) throws Exception { init(); ListVaultsRequest listVaultRequest = new ListVaultsRequest(); ListVaultsResult listVaultResult = glacier.listVaults(listVaultRequest); List<DescribeVaultOutput> listVault = listVaultResult.getVaultList(); for (DescribeVaultOutput vault : listVault) { System.out.println("getVaultName : "+vault.getVaultName()); System.out.println("getLastInventoryDate : "+vault.getLastInventoryDate()); System.out.println("getCreationDate : "+vault.getCreationDate()); System.out.println("getVaultARN : "+vault.getVaultARN()); System.out.println("getSizeInBytes : "+vault.getSizeInBytes()); System.out.println("getNumberOfArchives : "+vault.getNumberOfArchives()); System.out.println(); } } }
実行結果は以下のようになりました。
getLastInventoryDate : null getCreationDate : 2012-08-25T16:33:30.305Z getVaultARN : arn:aws:glacier:ap-northeast-1:XXXXXXXXXXXX:vaults/hoge getSizeInBytes : 0 getNumberOfArchives : 0 getVaultName : satoshi getLastInventoryDate : 2012-08-24T12:55:12.118Z getCreationDate : 2012-08-23T06:53:50.976Z getVaultARN : arn:aws:glacier:ap-northeast-1:XXXXXXXXXXXX:vaults/satoshi getSizeInBytes : 640039499 getNumberOfArchives : 8
アーカイブをアップロードする
ボルトにアーカイブをアップロードする方法も簡単です。以下をご覧ください。
import java.io.File; import java.util.Date; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.PropertiesCredentials; import com.amazonaws.services.glacier.AmazonGlacierClient; import com.amazonaws.services.glacier.transfer.ArchiveTransferManager; import com.amazonaws.services.glacier.transfer.UploadResult; public class AwsConsoleApp2 { private static AmazonGlacierClient glacier; private static AWSCredentials credentials; private static String vaultName = "satoshi"; private static String archiveToUpload = "/data/data3.dat"; private static void init() throws Exception { credentials = new PropertiesCredentials( AwsConsoleApp2.class .getResourceAsStream("AwsCredentials.properties")); glacier = new AmazonGlacierClient(credentials); glacier.setEndpoint("https://glacier.ap-northeast-1.amazonaws.com/"); } public static void main(String[] args) throws Exception { init(); long start = System.currentTimeMillis(); ArchiveTransferManager atm = new ArchiveTransferManager(glacier, credentials); UploadResult result = atm.upload(vaultName, "my archive " + (new Date()), new File(archiveToUpload)); System.out.println("Archive ID: " + result.getArchiveId()); long end = System.currentTimeMillis(); System.out.println("time : "+ (end-start) + " ms"); } }
実行結果は以下です。
Archive ID: p_RXXXXXNZSdz0Sp3CxpAXXXXXXXXXXXXXXr-VNN6n0lrDnRLf57MXQLExXXXXXXXXmFAGmhim5wt-VXXXXXXXw_Bj-rXXXXXXXk9pJrqkDlM5wvjFctXXXXXXXXXXXXXXXX time : 4166 ms
ArchiveTransferManagerというクラスが内部でよろしくやってくれています。アップロード後にアーカイブIDが発行されます。これは忘れずに保管して置いてください。紙に書く長さではないので、S3やDynamoDBに入れておけばいいかもしれませんね。Glacierさんは、長いお付き合いをして欲しいとアピールしてくるのに、連絡先は2度と教えてくれないツンデレ仕様です。
アーカイブをダウンロードする
import java.io.File; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.PropertiesCredentials; import com.amazonaws.services.glacier.AmazonGlacierClient; import com.amazonaws.services.glacier.transfer.ArchiveTransferManager; import com.amazonaws.services.sns.AmazonSNSClient; import com.amazonaws.services.sqs.AmazonSQSClient; public class AwsConsoleApp3 { private static AmazonGlacierClient glacier; private static AmazonSQSClient sqs; private static AmazonSNSClient sns; private static AWSCredentials credentials; private static String vaultName = "satoshi"; private static String downloadPath = "/data/download.dat"; private static String archiveId = "p_RXXXXXNZSdz0Sp3CxpAXXXXXXXXXXXXXXr-VNN6n0lrDnRLf57MXQLExXXXXXXXXmFAGmhim5wt-VXXXXXXXw_Bj-rXXXXXXXk9pJrqkDlM5wvjFctXXXXXXXXXXXXXXXX"; private static void init() throws Exception { credentials = new PropertiesCredentials( AwsConsoleApp3.class .getResourceAsStream("AwsCredentials.properties")); glacier = new AmazonGlacierClient(credentials); sqs = new AmazonSQSClient(credentials); sns = new AmazonSNSClient(credentials); glacier.setEndpoint("https://glacier.ap-northeast-1.amazonaws.com/"); sqs.setEndpoint("https://sqs.ap-northeast-1.amazonaws.com/"); sns.setEndpoint("https://sns.ap-northeast-1.amazonaws.com/"); } public static void main(String[] args) throws Exception { init(); long start = System.currentTimeMillis(); ArchiveTransferManager atm = new ArchiveTransferManager(glacier,sqs,sns); atm.download(vaultName, archiveId, new File(downloadPath)); long end = System.currentTimeMillis(); System.out.println("time : " + (end - start) + " ms"); } }
こちらもArchiveTransferManagerというクラスが内部でよろしくやってくれています。 downloadメソッドは約5時間か掛かります。内部で何をやっているのか確認してみましょう。以下はdownloadメソッドの中身です。ボルト名やアーカイブIDを指定してジョブを作成しています。そして、jobStatusMonitor.waitForJobToCompleteメソッドの中でポーリングをしています。
public void download(final String accountId, final String vaultName, final String archiveId, final File file) throws AmazonServiceException, AmazonClientException { JobStatusMonitor jobStatusMonitor = null; GetJobOutputResult jobOutputResult = null; try { if (credentialsProvider != null && clientConfiguration != null) { jobStatusMonitor = new JobStatusMonitor(credentialsProvider, clientConfiguration); } else { jobStatusMonitor = new JobStatusMonitor(sqs, sns); } JobParameters jobParameters = new JobParameters() .withArchiveId(archiveId) .withType("archive-retrieval") .withSNSTopic(jobStatusMonitor.getTopicArn()); InitiateJobResult archiveRetrievalResult = glacier.initiateJob(new InitiateJobRequest() .withAccountId(accountId) .withVaultName(vaultName) .withJobParameters(jobParameters)); String jobId = archiveRetrievalResult.getJobId(); jobStatusMonitor.waitForJobToComplete(jobId); jobOutputResult = glacier.getJobOutput(new GetJobOutputRequest() .withAccountId(accountId) .withVaultName(vaultName) .withJobId(jobId)); } finally { jobStatusMonitor.shutdown(); } downloadJobOutput(jobOutputResult, file); }
JobStatusMonitorクラスのwaitForJobToCompleteメソッドの中身を見てみましょう。SQSをポーリングしていますね。先ほど、ジョブの作成をしましたが、この際にSNS(通知)を指定していました。このSNSを通じてSQSにキューが登録されるようになっています。ですから、SQSにキューが登録されたらそれを取り出して中身の情報からアーカイブをダウンロードできるわけです。この非同期の処理手順は大変参考になります。粗結合にスケールする基本的なやり方だと思います。
public void waitForJobToComplete(String jobId) { while (true) { List<Message> messages = sqs.receiveMessage(new ReceiveMessageRequest(queueUrl)).getMessages(); for (Message message : messages) { sleep(1000 * 30); String messageBody = message.getBody(); if (!messageBody.startsWith("{")) { messageBody = new String(BinaryUtils.fromBase64(messageBody)); } try { JSONObject json = new JSONObject(messageBody); String jsonMessage = json.getString("Message").replace("\\\"", "\""); json = new JSONObject(jsonMessage); String messageJobId = json.getString("JobId"); String messageStatus = json.getString("StatusMessage"); // Don't process this message if it wasn't the job we were looking for if (!jobId.equals(messageJobId)) continue; try { if (StatusCode.Succeeded.toString().equals(messageStatus)) return; if (StatusCode.Failed.toString().equals(messageStatus)) { throw new AmazonClientException("Archive retrieval failed"); } } finally { deleteMessage(message); } } catch (JSONException e) { throw new AmazonClientException("Unable to parse status message: " + messageBody, e); } } } }
まとめ
Amazon Glacierは、個人にとっても企業にとっても大変魅力的なサービスです。個人で100GBのデータ資産があれば月に100円で保存できます。企業で10TBのデータ資産があれば月に1万円で保存できます。仮に1ページ10MBの文書データがあったとすると、1,000,000ページの分の紙を保存するスペースを削減できます。1冊で1,000ページだとしたら1,000冊分の場所、キャビネット10個分ぐらいでしょうか。1坪で2つのキャビネットと考えると、5坪分確保できます。1坪単価2万円だとすると10万円分の土地有効活用ができます。ついでに社内のサーバールームをデータセンターに置けばさらに2坪は空きますね。他のストレージサービスだと10TBで10万円ぐらいでしょうか。地代家賃とトントンと考えると導入はあまり進みませんね。Glacierは従来の10分の1の価格を実現することで、企業のデータを安全に長期に保存でき、空いた場所の地代家賃が浮いて大きなコスト削減になります。固定費削減の急先鋒ですね。国内には、都心の一等地に文書を保存するだけの倉庫がたくさんあると聞いた事があります。長期保存しようとするとコスト負担が大きいため短期間で捨ててしまうという判断で後から探せなくなることもあるとか。会計監査データやビジネス文書など、コンプライアンス上保管する必要のあるデータは世の中にたくさんあります。価格が一桁下がる事で導入が増え、一気に世の中が変わるのではと。そんな妄想をしながら、Glacierを簡単に使えるように導入支援などを行って行きたいと思っています。
参考資料
Amazon Glacier Developer Guide (API Version 2012-06-01)
SNS topics must be in the local AWS region
[ANN] FastGlacier - The first GUI Client for Amazon Glacier is released