S3バケットのオブジェクトを時間に合わせて公開してみた

PDFや静的ウェブサイトなど、S3に置いていて、かつ時限式で公開したいコンテンツがある場合に使えます。

こんにちは、AWS事業本部の荒平(@0Air)です。

定められた時間まで公開したくない資料をしばしば扱うことがあります。
サードパーティ製品で公開日時を設定して、時間が来たら表示できるようにする仕組みを持っているものもありますが、AWSサービスのみを使って実現できる方法を考えてみました。

なお、バケットポリシーの内容さえ変えれば、同じ方法で設定時間に資料公開を停止することも可能です。

構成図

本エントリの構成図は以下の通りです。(やり方は他にもあるかと思います)
CloudWatch Eventsにて公開時間の1分前にLambdaをキックし、時間になるまでポーリング、設定時間が来たらS3のバケットポリシーを公開に設定するといった流れです。

やってみた

実際に構築して動作を確認してみました。

1. S3バケットの準備

公開対象とするS3バケットを用意し、ファイルを配置します。
今回は、sample.pdfを配置しました。

最終的にファイルを公開するため、バケットのブロックパブリックアクセスは「オフ」の状態にします。
オフになっていない場合は「アクセス許可」→「編集」から操作できます。

アカウントレベルのブロックパブリックアクセスが有効になっている場合は、こちらもオフにしておきます。
「このアカウントのブロックパブリックアクセス設定」から設定できます。

この時点では、バケットポリシーは何も設定されていない状態です。

2. Lambda関数の作成

PythonランタイムのLambda関数を作成します。
9行目のバケット名、および37行目の公開時間は適宜修正が必要です。
なお、時間はUTCで扱われるため、JSTへの変換を入れています。

import boto3
import json
from botocore.exceptions import ClientError
import time
from datetime import datetime, timedelta

def lambda_handler(event, context):
    s3 = boto3.client('s3')
    bucket_name = 'sample-bucket-arap'
    
    new_policy = {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "AllowPublicReadAccess",
                "Effect": "Allow",
                "Principal": "*",
                "Action": ["s3:GetObject", "s3:ListBucket"],
                "Resource": [
                    f"arn:aws:s3:::{bucket_name}",
                    f"arn:aws:s3:::{bucket_name}/*"
                ]
            }
        ]
    }
    
    max_retries = 3
    retry_delay = 1  # seconds

    for attempt in range(max_retries):
        try:
            # 現在時刻(UTC)を取得し、JSTに変換(UTC+9)
            current_time_utc = datetime.utcnow()
            current_time_jst = current_time_utc + timedelta(hours=9)
            
            # 設定時間になるまで待機
            target_time_jst = current_time_jst.replace(hour=15, minute=0, second=0, microsecond=0)
            if current_time_jst < target_time_jst:
                wait_seconds = (target_time_jst - current_time_jst).total_seconds()
                time.sleep(wait_seconds)

            # バケットポリシーを更新
            s3.put_bucket_policy(Bucket=bucket_name, Policy=json.dumps(new_policy))
            print(f"Successfully updated bucket policy for {bucket_name} at {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC")
            return {
                'statusCode': 200,
                'body': json.dumps('Bucket policy updated successfully')
            }
        except ClientError as e:
            print(f"Error updating bucket policy (attempt {attempt + 1}): {e}")
            if attempt < max_retries - 1:
                time.sleep(retry_delay)
            else:
                return {
                    'statusCode': 500,
                    'body': json.dumps(f'Failed to update bucket policy after {max_retries} attempts')
                }

Lambdaのタイムアウト値は2分に設定します。

また、Lambdaには、S3バケットのポリシーを操作する権限を付与します。
以下は権限の例です。 10行目はバケット名を環境に合わせて変えてください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetBucketPolicy",
                "s3:PutBucketPolicy"
            ],
            "Resource": "arn:aws:s3:::<bucket-name>"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        }
    ]
}

3. トリガーの設定

CloudWatch Eventsにてトリガーを設定し、Enabledの状態にします。

cron式は、公開したい時間の1分前にします。これは事前にLambdaを起動して関数を待機させておくことで、極力公開までのラグを減らす仕組みです。
Lambdaのタイムアウト値を2分にしているのは、これが理由です。

cron式の例: cron(42 10 28 6 ? 2024)
この場合、2024年6月28日 10:42 (19:42 JST)に起動します。

なお、CloudWatch Logsにて、設定した時間になると(900msほど遅延しているものの)S3バケットのポリシーが書き換わっていることが分かります。

4. 動作確認

事前にS3バケットへ格納していたsample.pdfが確認できました。

なお、今回のバケットポリシーを設定していない状態(公開時間前など)でアクセスするとAccess Denied(403)が返されます。

おわりに

公開時間を指定してS3バケットのファイルを公開する方法を考えてみました。

検証では、バケットポリシーの更新によって実際の表示に影響があるまで、数秒遅延がある場合がありました。
ネットワーク状況やファイルサイズ、環境にもよりますので、運用前にドライランすることをお勧めします。

ミリ秒単位の精度が要求されない場合は、これでも問題なく利用できると思います。

このエントリが誰かの助けになれば幸いです。
それでは、AWS事業本部 コンサルティング部の荒平(@0Air)がお送りしました!