方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【AWS】S3署名付き(presigned)URLを使用して、音声合成ファイルのアクセス制限を管理する方法

s3-presigned-url
この記事は約12分で読めます。

はじめに

様々な方言を話すおしゃべり猫型ロボット「ミーア」を開発中。

https://mia-cat.com

現在、ベータ版リリース後の次の機能として、ユーザーがアプリでミーアに喋らせたい任意のテキストを再生時刻とともに入力したら、その時間でESP32から音声再生できるようにする機能を開発中。

ユーザーが作成したテキストは、アプリからサーバー側へAPIリクエストで送られたのちに、サーバー側で音声合成したのちにAWSの各ユーザーディレクトリ下のS3フォルダに格納する。

ただ、この音声フレーズはユーザーディレクトリ下なのでアクセス制限がデフォルトになっており、そのままだとESP32から音声ダウンロードできない。

なので、今回はpresigned URLを使用したいと思う。

プリサインURLとは?

プリサインURLは、クラウドストレージサービス(例:Amazon S3)内のオブジェクトへの一時的なアクセスを提供するURL

このURLは特定の権限と有効期限で署名され、クラウドストレージの資格情報に直接アクセスすることなく、安全にファイルをダウンロードまたはアップロードすることができる。

AWS公式サイト

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html

プリサインURLの仕組み

プリサインURLは、署名やその他のパラメーターをURL自体に埋め込むことによって機能する。この署名はクラウドストレージの資格情報を使用して生成され、URLが指定された制約内でのみ使用できることを保証する。

クエリパラメーターの具体例

プリサインURLには、通常以下のようなクエリパラメーターが含まれる:

  • X-Amz-Algorithm:署名に使用されたアルゴリズム(例:AWS4-HMAC-SHA256)
  • X-Amz-Credential:署名に使用されたAWS資格情報
  • X-Amz-Date:リクエストが作成された日時
  • X-Amz-Expires:URLの有効期限(秒単位)
  • X-Amz-SignedHeaders:署名に含まれるヘッダー情報
  • X-Amz-Signature:リクエストの署名
ShellScript
https://your-bucket-name.s3.amazonaws.com/your-object-key?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=YOUR_CREDENTIALS&X-Amz-Date=20240724T123456Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=YOUR_SIGNATURE

プリサインURLの有効期限設定

プリサインURLでは、URLの有効期限を指定できる。

これは、アクセスが許可される期間を制限するための重要な機能。今回の実装の場合、プリサインURLをサーバーで生成した直後にデバイスシャドウを更新し、デバイスシャドウが更新されたらすぐにESP32がダウンロードを開始するので、有効期限は短め(例えば2分)で設定する。

それでは、概念を理解したところで実装に入りたいと思う。

サーバー側(Go)

ミーアでは、ユーザーが入力したテキストを音声合成し、AWS S3に保存する。その際、ESP32から音声ファイルにアクセスするためにプリサインURLを生成する。

プリサインURL生成関数

  1. AWS Configの読み込み: AWSの設定をロード。
  2. S3クライアントの初期化: S3クライアントを設定。
  3. プリサインURLの生成: 指定されたバケットとオブジェクトキーでプリサインURLを生成。

synthesize_speech.go

Go
// presigned URL生成
func GeneratePresignedURL(ctx context.Context, bucketName, key string, expiry time.Duration) (string, error) {
	awsCfg, err := config.LoadDefaultConfig(ctx)
	if err != nil {
		return "", fmt.Errorf("failed to load AWS config: %v", err)
	}
	s3Client := s3.NewFromConfig(awsCfg)

	presignClient := s3.NewPresignClient(s3Client)
	req, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String(key),
	}, s3.WithPresignExpires(expiry))
	if err != nil {
		return "", fmt.Errorf("failed to generate presigned URL: %v", err)
	}
	return req.URL, nil
}

ユーザー定義フレーズの処理関数

  1. タイムゾーンの設定: JST(日本標準時)をロード。
  2. 現在の時間を取得: JSTでの現在の時間を取得し、フォーマット。
  3. データベースからスケジュールを取得: ユーザーのフレーズ情報を取得。
  4. プリサインURLの生成: 取得したvoice_pathでプリサインURLを生成。
  5. デバイスシャドウの更新: デバイスシャドウをプリサインURLで更新。

worker.go

Go

// ユーザー定義フレーズの処理
func ProcessUserPhraseMessage(ctx context.Context, db *sqlx.DB, message Message, config *Config, shadowManager ShadowManager) {
	// タイムゾーンをロード
	jst, err := time.LoadLocation("Asia/Tokyo")
	if err != nil {
		log.Fatalf("Failed to load the 'Asia/Tokyo' time zone: %v", err)
	}

	// 現在の時間をJSTで取得
	currentTimeJST := time.Now().In(jst)

	// クエリに使用する時刻と曜日をデバッグログに出力
	formattedTime := currentTimeJST.Format("15:04")
	weekday := currentTimeJST.Weekday().String()[:3]
	log.Printf("Query Time: %s", formattedTime)
	log.Printf("Query Weekday: %s", weekday)

	// スケジュールからユーザーフレーズ情報を取得
	var schedule struct {
		VoicePath string `db:"voice_path"`
	}
	err = db.Get(&schedule, "SELECT up.voice_path FROM phrase_schedules ps JOIN user_phrases up ON ps.phrase_id = up.id WHERE ps.user_id = ? AND TIME_FORMAT(ps.time, '%H:%i') = ? AND FIND_IN_SET(?, ps.days) > 0;", message.UserID, formattedTime, weekday)
	if err != nil {
		log.Printf("Failed to fetch user phrase schedule for user %d: %v", message.UserID, err)
		return
	}

	// デバッグログ: 取得したvoice_pathを出力
	log.Printf("Fetched voice path for user %d: %s", message.UserID, schedule.VoicePath)

	// プリサインドURLの生成
	s3Url, err := GeneratePresignedURL(ctx, config.AWSS3ApiBucket, schedule.VoicePath, 2*time.Minute)
	if err != nil {
		log.Printf("Failed to generate presigned URL for user %d: %v", message.UserID, err)
		return
	}
	log.Printf("Generated presigned URL: %s", s3Url)

	// デバイスシャドウを更新
	user, err := GetUser(db, message.UID)
	if err != nil {
		log.Printf("Failed to get user info for device shadow update: %v", err)
		return
	}
	log.Printf("Updating device shadow for user %d with presigned URL %s", message.UserID, s3Url)
	err = UpdateDeviceShadow(ctx, shadowManager, user.DeviceID.V, s3Url, "user_phrase")
	if err != nil {
		log.Printf("Failed to update device shadow for user %d: %v", message.UserID, err)
		return
	}
	log.Printf("Scheduled task completed for user: %d", message.UserID)
}

動作確認

サーバー側ログ

  • ユーザーの音声ファイルパスを取得。
  • 2分間有効なプリサインURLを生成。
  • プリサインURLをデバイスシャドウに更新。
  • 処理が完了したことを記録。
ShellScript
clocky_api_local  | 2024/07/23 21:44:00 Fetched voice path for user 1: users/1/user_phrase/user_phrase_20240721-140517.mp3
clocky_api_local  | 2024/07/23 21:44:00 Generated presigned URL: https://mia-dev-api.s3.ap-northeast-1.amazonaws.com/users/1/user_phrase/user_phrase_20240721-140517.mp3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=XXX%2F20240723%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20240723T214400Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&x-id=GetObject&X-Amz-Signature=XXX
clocky_api_local  | 2024/07/23 21:44:00 Updating device shadow for user 1 with presigned URL https://mia-dev-api.s3.ap-northeast-1.amazonaws.com/users/1/user_phrase/user_phrase_20240721-140517.mp3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=XXX%2F20240723%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20240723T214400Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&x-id=GetObject&X-Amz-Signature=XXX
clocky_api_local  | 2024/07/23 21:44:00 Scheduled task completed for user: 1

デバイス側ログ

  • デバイスがMQTTメッセージを受信し、プリサインURLを含むデバイスシャドウの更新を検知。
  • これにより、ESP32は音声ファイルのダウンロードを開始する。
ShellScript
06:45:05.157 > MQTTPubSubClient::onMessage: $aws/things/XXX/shadow/update/delta {"version":391,"timestamp":XXX,"state":{"config":{"user_phrase_audio_url":"https://mia-dev-api.s3.ap-northeast-1.amazonaws.com/users/1/user_phrase/user_phrase_20240721-140517.mp3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=XXX%2F20240723%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20240723T214400Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&x-id=GetObject&X-Amz-Signature=XXX"}},"metadata":{"config":{"user_phrase_audio_url":{"timestamp":XXX}}}}

MQTTメッセージで送られてくるuser_phrase_audio_urlを直接クリックすると、有効期限内だとダウンロードでき、有効期限を過ぎるとaccess deniedになることを確認できた。

タイトルとURLをコピーしました