見出し画像

署名付きURLを使って画像ファイルをアップロードするようにした

はじめに

こんにちは!tsurumiと申します。
くふうAIスタジオのサーバーサイドエンジニアとして、家計簿アプリZaim の開発を担当しています。

今回は、Zaim の画像アップロード機能の改善について紹介したいと思います。
このアップデートでは、AWS S3 を用いて、より安全かつ効率的に画像ファイルを扱えるように、署名付き URL によるアップロード方法への変更を実施しました。

改善の背景

Zaim では、ユーザーがレシートなどの画像を記録と共にアップロードする機能を提供しています。
元々は API 経由で S3 に直接画像をアップロードしていたのですが、これにより API に過剰な負荷がかかり、稀にエラーが発生する状況に直面していました。
また、新たに複数の画像を一度にアップロードする機能を実装することになり、アップロード機能の改善を検討することになりました。

機能改善に向けた要件定義

改善のプロセスでは、API の負荷軽減とセキュリティの確保を両立する新しいアプローチを模索しました。
セキュリティ上の懸念とコスト面での評価を含めた上で、Design Doc にまとめました。

主な懸念点は以下のようなものがありました。

  • 署名付き URL の発行に時間がかかる、または発行できない

  • CloudFront のキャッシュが効いて上書き後のファイルが表示されない

  • CloudFront の負荷

  • 署名付き URL、Cookie の発行コスト

  • CloudFront のコスト

  • 既存 API への負荷

  • 画像サイズの最適化やファイルの妥当性チェックはアプリ側で対応が必要

これらの課題を調査検討し、特に大きな問題はないと判断したため、実装フェーズへと進むことにしました。

実装のプロセス

互換性の保持

アップデートしていないアプリを利用していないユーザーのために、既存 API のアップロード機能や返却値の互換性を保ちつつ、新たな機能を追加する必要がありました。
実はこの辺りの整合性を保つのが一番大変でしたが、今回は署名付き URL の導入という記事の趣旨から外れるので割愛します。

署名付き URL の生成

新しく作成されたエンドポイントでは、以下の情報を含む署名付き URL が生成されます。
この署名付き URL の有効期限はセキュリティ上非常に短く設定しています。


署名付き URL を生成する API の返却値

署名付き Cookie の導入

アプリから直接画像ファイルを取得できるようにするため、CloudFront 経由の HTTP プロトコルで S3 バケットにアクセスできるようにします。その際に署名付き Cookie をリクエストヘッダに含めてもらい、アクセス制限を行います。

CloudFront を介したアクセスに署名付き Cookie を使用するようにしました。
これにより、ユーザーごとに発行された署名を用いて S3 バケットへ安全にアクセス可能になります。
そのため、以下の情報を返却する署名付き Cookie を生成するエンドポイントを新設しました。

署名付き Cookie を生成する API の返却値

署名付き Cookie はユーザーごとに発行し、有効期限は比較的長めに設計しました。
というのも、万が一署名付き Cookie が漏洩しても該当のユーザー以外の画像ファイルは表示できないように設計しているためです。

AWS への署名付き Cookie のリクエストは、以下のようにユーザー別のディレクトリ以下をワイルドカード指定することで、そのユーザーのリソースのみアクセスできるようにしています。

{
    "Statement": [
        {
            "Resource": "https://zaim.net/bucket/[user_id]/*",
            "Condition": {
                "DateLessThan": {
                    "AWS:EpochTime": 1622470423
                }
            }
        }
    ]
}

AWSの設定調整

CloudFront 経由の HTTP プロトコルで ユーザーごとの署名付き Cookie を HTTP ヘッダに付与し S3バケットにアクセスできるようにするため、AWS 側で以下の設定を行います。

  • CloudFront に新しいオリジンとビヘイビアの追加

  • パブリックキーやキーグループの設定

  • 新しい S3 バケットの作成とバケットポリシーの編集

Zaim では Terraform でインフラのコード管理を行なっているので、以下のような設定を追加しました。
(パラメータは実運用のものとは異なります)

# CloudFront オリジンアクセスの追加
resource "aws_cloudfront_origin_access_control" "sample-bucket" {
    description                       = "zaim-sample-bucket"
    name                              = "zaim-sample-bucket.s3.[region].amazonaws.com"
    origin_access_control_origin_type = "s3"
    signing_behavior                  = "always"
    signing_protocol                  = "sigv4"
}
# 公開鍵とキーグループの追加
module "public_key" {
  source = "./public_key/"
}
resource "aws_cloudfront_public_key" "zaim-sample-bucket-pubkey" {
    name             = "zaim-sample-bucket-pubkey"
    encoded_key      = <<-EOT
        -----BEGIN PUBLIC KEY-----
        ************************
        -----END PUBLIC KEY-----
    EOT
}
resource "aws_cloudfront_key_group" "zaim-sample-bucket" {
    name  = "zaim-sample-bucket"
    items = [
        aws_cloudfront_public_key.zaim-sample-bucket-pubkey.id,
    ]
}
output "zaim-sample-bucket-key-group-id" {
  value = aws_cloudfront_key_group.zaim-sample-bucket.id
}
# CloudFront ビヘイビアとオリジンを追加
resource "aws_cloudfront_distribution" "web" {
    ordered_cache_behavior {
        allowed_methods        = [
            "GET",
            "HEAD",
        ]
        cache_policy_id        = "*********"
        cached_methods         = [
            "GET",
            "HEAD",
        ]
        compress               = true
        default_ttl            = 0
        max_ttl                = 0
        min_ttl                = 0
        path_pattern           = "/[bucket]/*"
        smooth_streaming       = false
        target_origin_id       = "zaim-sample-bucket.s3.[region].amazonaws.com"
        trusted_key_groups     = [
            module.public_key.zaim-sample-bucket-key-group-id,
        ]
        trusted_signers        = []
        viewer_protocol_policy = "redirect-to-https"
    }
    origin {
        domain_name              = "zaim-sample-bucket.s3.[bucket].amazonaws.com"
        origin_access_control_id = "*****"
        origin_id                = "zaim-sample-bucket.s3.[bucket].amazonaws.com"
        connection_attempts      = 3
        connection_timeout       = 10
    }
}
# S3 バケットポリシーの設定
resource "aws_s3_bucket_policy" "zaim-sample-bucket-policy" {
    bucket = "zaim-sample-bucket"
    policy = jsonencode(
        {
            Statement = [
                {
                    Action    = "s3:GetObject"
                    Condition = {
                        StringEquals = {
                            "AWS:SourceArn" = "arn:aws:cloudfront::*****:distribution/*****"
                        }
                    }
                    Effect    = "Allow"
                    Principal = {
                        Service = "cloudfront.amazonaws.com"
                    }
                    Resource  = "arn:aws:s3:::zaim-sample-bucket/*"
                    Sid       = "AllowCloudFrontServicePrincipal"
                },
            ]
            Version   = "2008-10-17"
        }
    )
}

実運用を通じた課題

運用を開始してみると、幸いなことに現在まで大きな問題は発生していません。
ただ、実装時にはアップロードエラーやオフライン時の対応など、アプリ側の実装負荷が自分の想定より大きくなっていました。
この点に関しては、私のアプリ実装の知見が不足していたこともあり反省すべきところでした。
署名付き URL のアップロード機能に関しては他の試作でも応用可能で、デイリーメモの画像アップロードにも同様の方法が利用され、その実装経験が積まれました。

まとめ

Zaim では、画像ファイルを安全にアップロードするためのインフラを改善いたしました。
こうした技術的取り組みは、時にアプリ側の開発の工数を増加させることがあり、その点では事前のしっかりとしたコミュニケーションが重要になってきます。
今後も、新しいテクノロジーを採り入れ常に改善を続けることで、より良いソフトウェアとなっていくことを目指していきます。

くふうAIスタジオでは、採用活動を行っています

当社は「AX で 暮らしに ひらめきを」をビジョンに、2023年7月に設立されました。
(AX=AI eXperience(UI/UX における AI/AX)とAI Transformation(DX におけるAX)の意味を持つ当社が唱えた造語) くふうカンパニーグループのサービスの企画開発運用を主な事業とし、非エンジニアさえも当たり前にAIを使いこなせるよう、積極的なAI利活用を推進しています。
(サービスの一例:累計DL数1,000万以上の家計簿アプリ「Zaim」、月間利用者数1,600万人のチラシアプリ「トクバイ」等) AXを活用した未来を一緒に作っていく仲間を募集中です。 ご興味がございましたら、以下からカジュアル面談のお申込みやご応募等お気軽にお問合せください。

この記事が気に入ったらサポートをしてみませんか?