月: 2025年9月

  • クラウドストレージの料金調査(3)

    前回は、自動コスト最適化機能についてご紹介しました。
    今回はそのおまけとして、料金算出をツール化してみました。
    よろしければ、皆さまの前提条件に合わせて料金シミュレーションをお試しください。

    なお、より正確な見積もりが必要な場合は、各社が提供している公式の見積もりツールをご利用いただくことをおすすめします。


    前提

    保存サイズ(GB)
    取出しサイズ(GB)
    為替レート円/1ドル

    見積り

    製品 料金($/GB) 月額
    (円)
    保存 取出 転送
    S3 Standard 0.139
    S3 I. A. 0.1378
    S3 G. Instant R. 0.149
    S3 G. Flexible R. 0.1295
    S3 G. Deep A. 0.121
    S3 Express O. Z. 0.23856
    GCP Standard 0.14
    GCP Nearline 0.146
    GCP Coldline 0.146
    GCP Archive 0.1725
    R2 Standard 0.015
    R2 I. A. 0.01

    ※テキストボックスの数値を変更すると自動的に月額が再計算されます

    ※料金の初期値は2025年9月時点のものです

    ※略語説明

    • I. A.: Infrequent Access
    • G.: Glacier
    • R.: Retrieve
    • A.: Archive
    • O. Z.: One Zone
    • R2: Cloudflare R2

    【広告】

  • クラウドストレージの料金調査(2)

    前回は、料金と制約の調査結果について共有しました。
    今回は、AWS と GCP が提供する 自動コスト最適化機能 についてご紹介します。


    自動コスト計算とは?

    長期間アクセスされないファイルを、自動的に低コストなストレージ階層へ移動してくれる仕組みです。
    各サービスでの名称は以下の通りです。

    • AWS S3 Intelligent Tiering
    • GCP Cloud Storage Autoclass

    AWS の場合

    • 初期状態では Standard クラスに保存されます。
    • 30日 経過すると、自動的に Infrequent Access へ移行。
    • さらに 60日 経過すると、Glacier Instant Retrieve に移行します。
    • アクセスが発生すると、自動的に Standard に戻ります。
    • 128KB以下のファイル は対象外となり、Standardから移行しません。

    オプションを有効にすると、さらに低コストな下記のストレージクラスにも自動移行できます。

    • Glacier Flexible Retrieve
    • Glacier Deep Archive

    ※これらに移行した場合、即時のファイル取得はできなくなります。


    GCP の場合

    • 初期状態は Standard です。
    • 30日 経過で Nearline に移行。
    • さらに 60日 経過で Coldline に移行。
    • さらに 275日 経過で Archive に移行します。
    • アクセスが発生した場合、AWSと同様に Standard に戻ります。

    課金体系の違い

    通常のストレージと異なる課金体系があります。

    • 取出し料金が無料
    • ファイル管理料金が発生
      1,000ファイルあたり $0.0025/月(※2025年9月時点、AWS/GCP共通)

    見積もり例

    前回の前提条件に自動コスト計算を適用してみます。

    前提条件

    • 年に1回すべてのファイルを取出す
    • ファイル数1,000個

    1年間で各階層でのファイル滞在期間は次になります。

    • Standard 1ヶ月(1/12)
    • NearLine 2ヶ月(2/12)
    • ColdLine 9ヶ月(9/12)
    • Archive 0ヶ月

    ※GCP のストレージ階層名で表しましたがAWS でも似たような状態になります。
    ※GCPでは約1年後にArchiveへ移行する仕様ですが、今回の前提では1年未満で全ファイルを取り出すため、Archive への移行は発生しません。


    感想

    二番目のと三番目のストレージ階層の中間程度のコストになりました。
    取出し頻度と料金の関係が予測しづらいと思いました。

    例)

    • 毎月1回取出した場合はStandard とほぼ同じ料金
    • 1日に連続12回取出した場合はこの見積もりのような料金

    それでも、自動でコストを最適化してくれるのは大きな利点です。Standard より高くなることはなく、運用の手間を減らしつつ費用を抑えることができます。

    【広告】

  • クラウドストレージの料金調査(1)

    添付ファイルを登録できるアプリを作成するにあたって、ファイルの保存先としてどのクラウドストレージを選ぶべきか、個人的に調査してみました。今回は、調査結果を共有します。

    注目した要素

    料金は大事な要素ではありますが、制約条件も無視できません。

    コストに影響する主な要素は以下の通りです。

    • 保存(データを保管する料金)
    • 取出(ファイルアクセスにかかる料金)
    • 転送(ネットワーク経由でダウンロードする料金)

    一方、見落としがちな制約として、以下のようなものがあります。

    • 最低課金日数(例:30日間分の料金がかかる)
    • 最低ファイルサイズの課金単位(例:128KB未満でも128KB分課金される)
    • 取出し時間の制限(即時アクセスできない場合あり)

    これらを踏まえ、各クラウドストレージサービスを比較した表がこちらです。


    比較表

    ※2025年9月時点の調査結果です

    各項目の説明

    • 保存:ファイルをクラウド上に保存しておくための料金
    • 取出:ファイルを読み込む操作にかかる費用。※ネットワーク転送費用は含みません。
    • 転送:ファイルをダウンロードするためのネットワーク費用。EC2 など同一リージョンのサービスからアクセスする場合、無料になることもあります。
    • 最低日数:保存期間に関係なく、最低○日分の料金が発生する仕組みです。たとえば「30日」の場合、1日しか保存しなくても30日分課金されます。
    • 最低サイズ:ファイルサイズが小さくても、指定の最小単位(例:128KB)で課金されます。
    • 取出し時間:取出し時間の指定があるストレージは、即時ファイル取り出しができません。取出しリクエストを送り、指定時間の後にダウンロード可能となります。

    利用シナリオに基づく料金見積もり

    例えば、1日に写真3枚をメモと共に保存したとすると、1日およそ7Mb を保存していくと仮定します。

    • 1年間で約2.5GB(7MB × 30日 × 12か月 ÷ 1,000)
    • 20年間で約50GB

    年に1回全てのファイルを取り出すと仮定すると、毎月約4Gb (50/12=4.1)のダウンロードが発生します。
    為替レートは1ドル=150円として、これらの前提をもとに料金を試算しました。


    評価・感想

    • AWS S3 Glacier Deep Archive がもっとも安いですが、取出し時間の制約があるため、アプリから使うには不向きです。主にバックアップ向けです。
    • GCP Archive は即時取り出せる点は魅力ですが、最低日数が約1年と長めです。同じくバックアップ向けと言えます。
    • CloudFlare は取出しと転送が無料な点で非常に魅力的です。一方、リージョンを指定できないため、利用規約やデータ保管ポリシーに影響する可能性があり注意が必要です。
    • AWS S3 Express One Zone は高性能低遅延がウリで、今回の安価なストレージを調査する趣旨と異なるのですが、試しに計算してみました。予想通り高額ですが、性能重視のアプリには良いかもしれません。

    今回はここまでです。
    次回は、自動割り当てについてご紹介します。

    【広告】

  • SwiftUIでスクロールに応じてメニューを隠す処理を実装してみた

    スマホアプリは小さな画面を有効活用するため、必要のない時はメニューを隠しておくとUX 向上につながります。
    メニューを常時表示する方が実装は簡単ですが、今回はスクロールに応じて表示・非表示を切り替えるUIを実装してみました。


    スクロール位置の取得

    Swift でスクロール位置を取得するには PreferenceKey を使用します。これは、スクロールする View と位置を受け取りたい親 View 間で値を連携する手段です。
    今回は、LazyVStack のスクロール位置を取得して、ContentsView で位置を受け取りメニュー表示の判断を行います。
    私はjavascript のように、window.scrollY で取得できるだろうと安易に考えていましたが、それよりは少し複雑でした。

    スクロールするView

    var body: some View {
        ScrollView {
            LazyVStack(alignment: .leading, spacing: 0) {
                // 大量のコンテンツ(省略)
            }
            // padding については後述
            .padding(.top, topMenuHeight)
            .padding(.bottom, bottomMenuHeight)
            .background(
                GeometryReader {
                    Color.clear.preference(
                        key: ScrollOffsetPreferenceKey.self,
                        // 画面上部のスクロール位置を取得してPreferenceKey で連携
                        value: $0.frame(in: .scrollView).minY
                    )
                }
            )
        }
    }

    Color.clear は画面に何も表示しないです。.preference を設定するために使用しています

    位置を取得するContentsView

    var body: some View {
        GeometryReader { geo in
            ZStack(alignment: .top) {
                // 画面コンテンツ(省略)
            }       .onPreferenceChange(ScrollOffsetPreferenceKey.self) { newY in
                // スクロール発生時にnewY が連携される
                refreshMenu(newY: newY, geo: geo)
            }
        }
    }

    PreferenceKey の実装(お作法に従ったもの)

    struct ScrollOffsetPreferenceKey: PreferenceKey {
        static var defaultValue: CGFloat = 0
        static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
            value += nextValue()
        }
    }

    Color.clear.preference から.onPreferenceChange へスクロール位置が連携されます。


    メニュー表示判定

    判定自体は比較的簡単です。
    スクロール位置は1フレーム毎に送付されるので、前回のスクロール位置を記憶しておくことで差分を算出できます。
    私は差分を15(ポイント)で判断するのが良いと思いました。
    そのため、+15 以上であれば下向きスクロールなのでメニューを隠す、-15 以下であれば、上向スクロールなのでメニューを表示する、これが基本になります。

    加えて、スクロールの上部に来た時はメニューが常に表示された方が良いです。
    スクロール位置が10 以下の場合は差分に関係なく表示するようにしました。

    private func calcMenuVisibilityAction(_ newY: CGFloat, _ oldY: CGFloat) -> MenuVisibilityActionEnum {
        let threshold: CGFloat = 15
        let topArea: CGFloat = 10
        if topArea < newY || topArea < oldY {
        // 上部に近い場合は常にメニューを表示
            return .show
        }
        let delta = newY - oldY
        if threshold < delta {
            // 上スクロール:メニューを表示
            return .show
        } else if threshold < -delta {
            // 下スクロール:メニューを隠す
            return .hide
        }
        // その他は何もしない
        return .none
    }

    メニュー位置の算出

    上のメニューについて考えてみます。
    表示するときは、上メニューのY座標を0 にすることで表示されます。こちらは簡単です。
    隠す時は少し厄介で、iPhone では、画面上部のベゼル部分(ノッチなど)を考慮する必要があります。
    メニューの高さにベゼルの高さを加えた分を画面外へ移動させます。

    ベゼルの高さはGeometoryReader のsafeAreaInsets.top で取得できます。
    画面外へのオフセットはこのようなコードで取得できます。(topMenuHeight は固定値)
    topMenuOffset = 0 – geo.safeAreaInsets.top – topMenuHeight

    下のメニューも同様にベゼルを考慮しつつ、画面外に移動させる量を算出します。


    スクロールView に余白を追加

    LazyVStack に.padding で上下メニューの高さを追加します。
    これがないと、メニューの裏側にコンテンツが隠れてしまいます。

    ただし、一番下までスクロールした際に、.padding(.bottom) で追加した余白がそのまま表示されてしまいます。
    見た目的に少し間延びする印象があるのですが、完全に解決するのは難しく、今回は割り切ってそのままにしています。
    ちなみに、GmailのiOSアプリでも同じような余白が見られたため、それほど大きな問題にはならないと判断しました。


    まとめ

    • PreferenceKey を使ってスクロール座標を取得する
    • 前回のスクロール位置と比較することで、メニュー表示の判定が可能
    • ベゼルを考慮してメニューを隠すときの表示位置を算出
    • メニューが重ならないように padding を入れる必要がある

    ソース

    【広告】

  • Java からBigQuery へのデータ投入してみた

    アクセスログをBigQuery に蓄積して解析したい、との要望があり、Java からBigQuery を操作する方法を調査・検証してみました。

    手順としては以下のようになります。


    1. プロジェクトの作成とBigQuery API の有効化
    2. データセット作成
    3. テーブル作成
    4. サービスアカウント作成
    5. Java からデータ挿入

    1. プロジェクトの作成とBigQuery API の有効化

    まずは Google Cloud Console にアクセスしてプロジェクトを作成します。

    ご注意:クレジットカードを登録しておかないと、select 文を発行することができません。今回の検証範囲であれば無料枠内に収まります。

    プロジェクト作成後、左側メニューから「API とサービス」>「ライブラリ」 を開きます。

    BigQuery API を検索します。

    有効にします。


    2. データセット作成

    BigQuery コンソールにて、プロジェクト名の横にあるケバブメニュー(三点アイコン)から「データセットを作成」を選択します。

    データセットに必要な情報を入力します。今回は、appdataset とリージョン東京を指定しました。

    ※後から知ったのですが、データセットIDにはアンダースコア(_)も使えるので、単語の区切りに利用すると読みやすくなります。

    作成が終わると、メニューにデータセットが表示され、クリックすると詳細が確認できます。


    3. テーブル作成

    無題のクエリータブを開いて、以下のようなDDL を実行してテーブルを作成します。

    create table appdataset.apptable (id int, memo string)

    BigQuery の DDL は PostgreSQL などと非常に似ているため、SQL に慣れていれば迷わず操作できると思います。

    「無題のクエリ」というタブを選ぶか、「+」を押すとクエリーを発行できます。実行を押すとテーブルが作成されます。


    4. サービスアカウントの作成

    次に、Java から BigQuery にアクセスするための認証情報を設定します。

    「IAMと管理」 > 「サービスアカウント」を選択します。

    「+サービスアカウントを作成」をクリック

    お好きなサービスアカウント名を入力します。今回は「bqtest」としました。

    権限を設定します。最低限必要な権限は以下の通りです。

    • データ挿入のみ 
        BigQuery データ編集者
    • クエリ実行も行う場合 
        BigQuery データ閲覧者
       BigQuery ジョブユーザー

    アクセス権を持つプリンシパルは省略しました。

    アカウント作成後は鍵の作成を行います。操作にあるケバブメニューから「鍵を管理」を選びます。

    「キーを追加」> 「新しい鍵を作成」を選択します。

    タイプをJSON にして作成すると、JSON ファイルがダウンロードされます。

    この JSON ファイルは後ほど Java コードの環境変数で使用するので、安全な場所に保管し、ファイルパスを控えておいてください。


    5. Java からデータ挿入

    依存ライブラリをgradle (or maven) に追加します。

    implementation("com.google.cloud:google-cloud-bigquery:2.54.2")

    このようなjava コードを書きます。

    package org.example;
    
    import com.google.cloud.bigquery.*;
    import java.util.*;
    
    public class Main {
    
        public static void main(String[] args) throws Exception {
            // BigQuery クライアントの作成
            BigQuery bigquery = BigQueryOptions.getDefaultInstance().getService();
    
            // テーブル参照
            TableId tableId = TableId.of("appdataset", "apptable");
    
            // 挿入する行データの定義(カラム名と値のペア)
            Map<String, Object> rowContent = new HashMap<>();
            rowContent.put("id", new Date().getTime());
            rowContent.put("memo", new Date().toString());
    
            // RowToInsert オブジェクトを作成
            InsertAllRequest.RowToInsert row = InsertAllRequest.RowToInsert.of(rowContent);
    
            // 挿入リクエストの作成
            InsertAllResponse response = bigquery.insertAll(
                    InsertAllRequest.newBuilder(tableId)
                            .addRow(row)
                            .build()
            );
    
            // エラーハンドリング
            if (response.hasErrors()) {
                System.out.println("エラーが発生しました:");
                response.getInsertErrors().forEach((key, err) -> System.out.println(err));
            } else {
                System.out.println("データを正常に挿入しました。");
            }
        }
    }

    このプログラムを実行する際、環境変数に先ほどダウンロードしたJSONファイルのパスを指定する必要があります。

    GOOGLE_APPLICATION_CREDENTIALS=/opt/testpj-3866e9977d0a.json

    intelliJ を使っている場合は、構成の編集から環境変数を指定できます。


    まとめ

    一連のセットアップが完了すれば、Java からBigQuery へデータ挿入するのはとてもスムーズでした。

    JDBC での操作とほとんど変わらない印象です。

    【広告】