はじめに
お疲れ様です。矢儀 @yuki_ink です。
コンプラ都合で、ドキュメントを長期間保存しないといけない!
可能な限りコストは下げたい!
・・・S3 Glacierだ!
WindowsからSMBでファイルの格納/取得をできるようにしたい!
・・・Storage Gatewayだ!!
ということで、「Storage Gateway Glacier」などとググると、以下のAWS公式ブログを見つけました。
今回やること
上記のブログを参考に環境構築してみた、というのが今回の内容です。
まず、ブログの内容をざっくりご紹介します。
【前提】
Storage Gateway(今回はFile Gateway)の裏にあるS3バケットでライフサイクルを設定し、オブジェクトをGlacierに移行する。
【課題】
ユーザがGlacierに移行されたオブジェクトにアクセスしようとすると、IOエラー(ファイルにアクセスできません!)が発生する。
オブジェクトをGlacierから復元する必要があるが、普段Storage Gateway に甘やかされている を使っているユーザにわざわざマネコンを操作させるというのもイケてない。
【解決策】
IOエラーが発生したら、それをトリガーとして、自動で対象のオブジェクトをリストアする仕組みを作ればいいじゃない!
今回はこれを実装していきます。
手順は以下の通りです。
- Storage Gateway(File Gateway)の設定
- Glacierへオブジェクトを移行するライフサイクルの設定
- 通知用SNSトピックの設定
- オブジェクト復元用Lambda関数の設定
- CloudWatch LogsとLambda関数の接続
- オブジェクト復元完了通知を受け取るための設定
1. Storage Gateway(File Gateway)の設定
こちらの記事を参考にして進めます。
Storage Gateway(File Gataway)を利用するための全てのステップが丁寧に記載されていました!感謝!
- 対象S3バケットの作成
- Gatewayインスタンス用サブネットの作成
- Gatewayインスタンス用ルートテーブルの作成
- VPCの設定変更
- S3用VPCエンドポイントの作成
- ゲートウェイの作成
- VPCエンドポイントの作成
- ゲートウェイのアクティブ化
- ファイル共有の作成
- Windowsからのマウント
詳細な手順は上記の記事に委ねますが、留意点をいくつか残しておきます。
CloudWatch Logs へのログ出力設定
ゲートウェイのアクティブ化
のところで、CloudWatch Logs にログを吐く設定をしておきましょう。
これでStorage Gatewayのログを収集できるようになります。
Windowsからのマウント
ファイル共有を作成する際、認証方法を「ゲストアクセス」にしたのですが、私のケースだと、「ファイル共有」のページに表示されているコマンドにユーザの表記がありませんでした。
なので、このコマンドをそのまま使ってWindowsからマウントしようとすると、ユーザ名とパスワードの入力が求められます。
そのような場合、ユーザ名には (ゲートウェイID)\smbguest
を入れます。
パスワードには、ファイル共有を作成する際に指定したものを入れます。
※ゲートウェイIDは、「ゲートウェイ」のページから確認できます。
コマンドプロンプトの入力は以下のようになります(デバイスレターがYの場合)
>## マネコンに表示されているコマンドを実行
>net use Y: \\172.31.24.50\standard-bucket-for-storage-gateway
Enter the user name for '172.31.24.50': sgw-A3B102CA\smbguest
Enter the password for 172.31.24.50: ********
The command completed successfully.
無事にマウントできました!
Windowsのエクスプローラーからファイルを配置できます。
(余談)プライベートサブネットでStorage Gatewayを作りたい場合
今回はSHIFTさんの記事を参考にパブリックサブネットでStorage Gatewayを構築しましたが、プライベートサブネットで構築したい場合は、以下の記事が参考になると思います。
2. Glacierへオブジェクトを移行するライフサイクルの設定
こちらの記事を参考にして進めます。
続いて、ライフサイクルルールを設定します。
今回は、以下のような設定をすることで、全てのオブジェクトを即座にGlacier Deep Archiveに移行するようにします。
現行バージョンの移行:
移行先: Glacier Deep Archive
オブジェクトの作成から: 0日後
非現行バージョンの移行 (バージョニングが有効な場合):
移行先: Glacier Deep Archive
オブジェクトが非現行になってから: 0日後
test2.pdf
というファイルをStorage Gateway経由で配置してみます。
S3の画面上で確認で確認すると・・・あれ、スタンダード?
ライフサイクルルールで、すぐGlacierに移行されると思ってたんですが(;'∀')
調べてみると、UTCの0時まで待ちなされということでした。
S3 ライフサイクルは 1 日に 1 回のみ実行されます。さらに、Amazon S3 はオブジェクトの移行日または有効期限を翌日の午前 0 時 (UTC) に四捨五入します。たとえば、2020 年 1 月 1 日 10:30 UTC に、3 日後にオブジェクトを移行するライフサイクルルールを使用してオブジェクトを作成したとします。このオブジェクトの場合、移行日は 2020 年 1 月 5 日 00:00 UTC です。ライフサイクルルールが満たされているかどうかを確認する前に、十分な時間が経過していることを確認してください。
というわけで、しばらく放置(2日くらい放置しました)。
再び確認すると、無事にGlacier Deep Archiveに移行できたようです。
128 KB 未満のオブジェクトは、デフォルトではどのストレージクラスにも移行されません!
ご注意ください!
128 KB 未満のオブジェクトは、デフォルトではどのストレージクラスにも移行されません。
Amazon S3 は S3 ライフサイクル設定にデフォルトの動作を適用し、128 KB 未満のオブジェクトが任意のストレージクラスに移行されないようにします。128 KB 未満のオブジェクトの移行はお勧めしません。オブジェクトごとに移行リクエストが課金されるためです。つまり、オブジェクトが小さいほど、移行コストがストレージの節約額を上回る可能性があります。移行リクエストのコストの詳細については、Amazon S3 料金ページのストレージとリクエストタブの「リクエストとデータの取得」を参照してください。小さいオブジェクトを移行できるようにするには、カスタム最小サイズ (ObjectSizeGreaterThan) または最大サイズ (ObjectSizeLessThan) を指定するオブジェクトサイズフィルターをライフサイクル移行ルールに追加できます。詳細については、「例: 128 KB 未満のオブジェクトの移行を許可する」を参照してください。
(出典)移行に関する制約と考慮事項
ちなみに・・・
S3上でオブジェクトがGlacierに移行されたからと言って、それが即ち「Storage Gateway(File Gataway)から開けなくなる」ということではありません。
そのファイルが頻繁に開かれるなどして、Storage Gateway(File Gataway)のキャッシュとして残っていれば、普通にそのファイルは開けます。
今回は検証を進めるために、Storage Gateway(File Gataway)の裏にあるS3バケットに直接ファイルを配置することで、疑似的にキャッシュが消えた状況を再現しました。
-
kensho_glacier.pdf
とkensho_standard.pdf
を用意し、S3バケットに直接格納(ファイル名が違うだけで、内容は同じPDFファイルです) -
kensho_glacier.pdf
は手動操作(「アクション」>「ストレージクラスを編集する」)でGlacier Deep Archiveへ移行
▼そのオブジェクトがWindowsのエクスプローラー画面からも確認できる
▼ kensho_glacier.pdf
を開くとIOエラーが発生
▼ kensho_standard.pdf
を開くと、正常にPDFが表示される(IOエラーは発生しない)
これで、「Standardに配置されているオブジェクトはStorage Gateway経由で開けるが、Glacierに配置されているオブジェクトは(キャッシュがなければ)Storage Gateway経由で開けない」 という状況を作ることができました。
3. 通知用SNSトピックの設定
こちらの記事を参考にして進めます。
アクセスポリシーは以下のように設定しておきます。
arn:aws:sns:ap-northeast-1:123456789012:MySNSTopic
の部分は、ご自身が作成するSNSトピックのARNに置き換えてください。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3ToPublishToSNS",
"Effect": "Allow",
"Principal": {
"Service": "s3.amazonaws.com"
},
"Action": "SNS:Publish",
"Resource": "arn:aws:sns:ap-northeast-1:123456789012:MySNSTopic",
"Condition": {
"ArnLike": {
"aws:SourceArn": "arn:aws:s3:::*"
}
}
},
{
"Sid": "AllowLambdaToPublishToSNS",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "SNS:Publish",
"Resource": "arn:aws:sns:ap-northeast-1:123456789012:MySNSTopic",
"Condition": {
"ArnLike": {
"aws:SourceArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:*"
}
}
}
]
}
メール通知を行うSNSトピックができました!
サブスクリプションの設定も忘れずにやっておきましょう!
4. オブジェクト復元用Lambda関数の設定
今回、LambdaのランタイムはPython 3.13を利用します。
コード、環境変数、アクセス権限、タイムアウト値は、それぞれ以下のように設定します。
4.1. コード
原典で紹介されているPythonのコードが古かったので、Python3に対応する形に修正しました。
また、通知周りを充実させました。
コードは以下の通りです。
import os
import json
import boto3
import base64
import gzip
from botocore.exceptions import ClientError
from io import BytesIO
def lambda_handler(event, context):
# CloudWatch Logsデータを取得し、base64デコードしてgzip解凍
cw_data = event['awslogs']['data']
cw_logs = gzip.GzipFile(fileobj=BytesIO(base64.b64decode(cw_data))).read()
log_events = json.loads(cw_logs)
# 各ログエントリを処理
result = None
for log_entry in log_events['logEvents']:
result = process_recall(log_entry)
print(result)
return {
'statusCode': 200,
'body': result
}
def process_recall(log_entry):
print("message contents: " + log_entry['message'])
message_json = json.loads(log_entry['message'])
print(message_json)
# SNSクライアントの作成
sns_client = boto3.client('sns')
# エラータイプを確認
if 'type' in message_json:
print("Found ErrorType")
error_type = message_json['type']
print("ErrorType = " + error_type)
if error_type != "InaccessibleStorageClass":
# エラータイプが異なる場合のSNS通知
sns_topic_arn = os.getenv('SnsTopicArn')
sns_client.publish(
TopicArn=sns_topic_arn,
Message=f"Unexpected error type '{error_type}' received, which is not related to storage class.",
Subject="S3 Object Restoration Not Initiated"
)
return "Unexpected error: not related to storage class"
else:
return "error: no type entry"
# バケット名とオブジェクトキーを確認
s3_bucket = message_json.get('bucket')
s3_key = message_json.get('key')
if not s3_bucket:
return "error: no bucket"
if not s3_key:
return "error: no key"
# 環境変数の確認と設定
restore_days = os.getenv('RestoreDays')
recall_tier = os.getenv('RecallTier')
sns_topic_arn = os.getenv('SnsTopicArn')
if not restore_days or not recall_tier or not sns_topic_arn:
return "error: missing required environment variables"
# SNS通知を送信(復元開始通知)
try:
sns_message = (f"Restoration started for object '{s3_key}' in bucket '{s3_bucket}' "
f"because the error type was '{error_type}', indicating an inaccessible storage class.")
sns_client.publish(
TopicArn=sns_topic_arn,
Message=sns_message,
Subject="S3 Object Restoration Started"
)
except ClientError as e:
print(f"Failed to send SNS notification: {e}")
return "error: SNS notification failed"
# S3オブジェクトの復元処理
s3 = boto3.resource('s3')
s3_object = s3.Object(s3_bucket, s3_key)
try:
result = s3_object.restore_object(RestoreRequest={
'Days': int(restore_days),
'GlacierJobParameters': {'Tier': recall_tier}
})
# 復元処理が正常にリクエストされた場合のSNS通知
sns_message = (f"Restoration request for object '{s3_key}' in bucket '{s3_bucket}' "
"has been successfully initiated. The restoration process may take several hours, "
"and you will be notified once it is complete.")
sns_client.publish(
TopicArn=sns_topic_arn,
Message=sns_message,
Subject="S3 Object Restoration Request Successful"
)
except ClientError as e:
# エラー発生時のSNS通知
if e.response['Error']['Code'] == 'RestoreAlreadyInProgress':
error_message = "Restoration is already in progress for this object."
else:
error_message = f"Unexpected error while attempting to recall object: {e}"
sns_client.publish(
TopicArn=sns_topic_arn,
Message=f"Failed to initiate restoration for object '{s3_key}' in bucket '{s3_bucket}': {error_message}",
Subject="S3 Object Restoration Failed"
)
return error_message
print(result)
return result
4.2. 環境変数
以下のように環境変数を設定しておきます。
キー | 値(例) | 説明 |
---|---|---|
RecallTier | Standard | 取り出しオプション(復元のスピード)を指定 |
RestoreDays | 1 | 復元したオブジェクトが一時的にアクセス可能となる日数を指定(整数値) |
SnsTopicArn | arn:aws:sns:(リージョン):(アカウントID):(トピック名) | 復元開始時やエラー発生時の通知を送信するSNSトピックのARN |
RecallTier
に指定できる値は以下の通りです。
S3 Glacier Flexible Retrieval ストレージクラスでは、Expedited、Standard、Bulk の取り出しオプションを使用できます。
ただし、S3 Glacier Deep Archive ストレージクラスに使用できるのは、Standard または Bulk の取り出しオプションのみです。
各オプションの詳細は以下を確認してください。
(出典)アーカイブ取得オプションを理解する
4.3. アクセス権限
Lambda関数に紐づくIAMロールには、オブジェクトの復元やSNSトピックへの通知などに必要な権限を付与しておきます。
今回は以下のポリシーを適用します。
※"Resource"
の値は適宜修正してください。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:RestoreObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::my-storage-gateway-bucket-name/*"
},
{
"Effect": "Allow",
"Action": [
"sns:Publish"
],
"Resource": "arn:aws:sns:my-region:my-account-id:MySnsTopic"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
4.4. タイムアウト値
デフォルトの3秒だとタイムアウトしてしまうので、タイムアウト値を伸ばします。
5. CloudWatch LogsとLambda関数の接続
まず、CloudWatch Logsの画面から、Storage Gatewayのログが出力されているロググループを選択し、ファイル共有ごとにログストリームが作成されていることを確認します。
ログストリームからログを覗いてみると、以下のようなログが出ているはずです。
▼ kensho_glacier.pdf
を開いてIOエラーが発生したときのログ
{
"severity": "ERROR",
"bucket": "standard-bucket-for-storage-gateway",
"s3Key": "kensho_glacier.pdf",
"roleArn": "(IAMロールのARN)",
"source": "share-44EE8C21",
"type": "InaccessibleStorageClass",
"operation": "s3:GetObject",
"key": "kensho_glacier.pdf",
"gateway": "sgw-A3B102CA",
"timestamp": "1731903680519"
}
ここまで確認できれば、CloudWatch LogsとLambda関数を接続していきましょう。
先ほど作成したLambda関数の設定画面から、トリガーを設定します。
トリガーの設定は以下の通りです。
項目 | 値(例) |
---|---|
ソース | CloudWatch Logs |
ロググループ | ※Storage Gatewayのログが出力されているロググループ |
フィルターの名前 | ※任意の値(FGWLogsFilter など) |
フィルターパターン | ERROR |
原典ではフィルターパターンとして InaccessibleStorageClass
を設定していますが、今回はあえて ERROR
を設定します。
※エラーが発生した際、オブジェクトの復元を待てばいいのか、あるいは別のアクションが必要なのか、ユーザ側で判断できるようにするため、とりあえず全てのエラー通知をLambda関数に連携し、エラーの内容に応じてユーザに通知を行うようにします。
6. オブジェクト復元完了通知を受け取るための設定
こちらの記事を参考にして進めます。
復元が完了しました (s3:ObjectRestore:Completed)
にチェックを入れておきます。
今回はついでに 復元されたオブジェクトの有効期限切れ (s3:ObjectRestore:Delete)
にもチェックを入れておきました。
復元が開始されました (s3:ObjectRestore:Post)
にもチェックを入れてもいいですが、Lambdaからの通知と被ってしまうので、いったんなしで。
やってみた
まず、Windowsのエクスプローラーから kensho_glacier.pdf
を開いて、IOエラーを発生させます。
CloudWatch Logsにも、エラーログが出力されました。
Lambda関数も正常に動いたようです。
▼ Lambda関数のログ
通知メールもちゃんと届いていました!
▼「リストア処理が始まったよ」
▼「復元のリクエストは正常終了したよ。復元完了したらまた通知行くから、それまで待ってね。」
S3から kensho_glacier.pdf
を確認すると、「復元が進行中」という表示がされていました。
よさそう!
待つこと12時間(長いがDeep Archiveなので仕方ない)、無事に復元が完了!
「復元の完了」という表示がされ、有効期限も示されています。
通知メールはこのような形で届いていました。
生のJSONではありますが、、笑
"eventName":"ObjectRestore:Completed"
の記載から、内容は分かります。
そして、Windowsのエクスプローラーからも問題なく kensho_glacier.pdf
が開けました!
オブジェクト復元後、復元の有効期間中はIOエラーは発生しないことが確認できました。
※復元の有効期限が切れても、Storage Gateway側にキャッシュが残っていれば、しばらくはそのままファイルを開けます。
Tips
最初はCloudFormationでStorage Gatewayを作ろうとしたのですが、思わぬ苦労があったので供養します。
そもそもStorage GatewayがCloudFormationに対応していなかった・・・!!
CloudFormationでやろうと思うと、Storage Gateway用のEC2を作るところまでが限界です。
Storage Gateway用に専用のAMIが用意されているので、それを使ってEC2を作ります。
ただ、そのEC2を使ってStorage Gatewayを作ろうとすると、EC2からアクティベーションキーを取得する必要があります。
それをCloudFormationでやろうとするとかなり大変だったので諦めました😅
CloudFormationでStorage Gateway用のEC2を作って、ユーザーデータを使ってアクティベーションキーの取得など色々やろうとしたのですが、ユーザーデータのスクリプトが動かず。
調査のため、Storage Gateway用のEC2にSSHしたらこんな感じでした。
これはダメそーー!!
Storage Gateway専用AMI感がありますね。
一応「6: Command Prompt」を選択してみて、適当にコマンドを打ってみます。
・・・基本的なコマンドさえ打てない!
ということで、Storage Gatewayを作るときは、今のところは大人しくマネジメントコンソールから操作することをおすすめします。
終わりに
コストを抑えるための手段としてGlacierは非常に強力な手段ですが、ファイルのサイズや更新・取り出しの頻度によっては、むしろStandardより利用料が高額になってしまうことがあります。
ご注意ください!!