Switching from S3 to Tigris on Fly.io

Ben Hoyt

Fly.ioでS3からTigrisへ乗り換える

原文は Ben Hoyt により に公開されました。 このブログを購読する

1年ほど前、サイドプロジェクトのGiftyWeddings.comのホスティングをAmazon EC2からFly.ioに切り替えました。作業自体も楽しかったですし、毎月数ドル節約にもなりました。

先週、Tigrisの担当者から連絡があり、Fly.ioのインフラ上に構築されfly CLIとも統合されたS3互換ストレージサービスのプライベートベータを試してみないかと声をかけられました。(Tigrisからはベータを試して記事を書くことに対して少額の報酬をいただいていますが、内容には関与していません。)

正直Tigrisのことは知らなかったのですが、興味を惹かれました。Fly.ioには良い印象を持っていますし、AWSという巨大企業――“Bezomoth”(BehemothとBezosをかけた造語)――に小さな会社がどんどん挑戦していくのは素晴らしいことだと思います。

Giftyのサーバー自体(Goで書かれています)はすでにFly.ioで動かしていましたが、数日前まではファイルストレージには依然としてAmazon S3を使っていました。ユーザーがアップロードした画像やSQLiteのバックアップ用です。

切り替えはシンプルでした。ファイルをコピーし、サーバーの設定をS3ではなくTigrisのエンドポイントを指すように更新して、fly deployするだけです。Tigrisでは認証やキャッシュの扱いが異なるため軽微なコード修正が必要だったほか、いくつか気になる挙動もありましたが、全体としては非常にスムーズでした。

バケットの作成

最初のステップは、ユーザーがアップロードした画像用のテストバケットを作成することでした。さっそくfly storage createコマンドを実行しました。

$ cd gifty  # change to app directory
$ fly storage create --public
? Choose a name, use the default, or leave blank to generate one:
    test-gifty-registry-images
Your Tigris project (test-gifty-registry-images) is ready. See details and
next steps with: https://fly.io/docs/reference/tigris/

Setting the following secrets on gifty:
BUCKET_NAME
AWS_ENDPOINT_URL_S3
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_REGION

Updating existing machines in 'gifty' with rolling strategy

-------
 ✔ Machine 4d89601c6e9487 [app] update succeeded
-------
Checking DNS configuration for gifty.fly.dev

バケットを作成しただけで一連の環境変数が自動的に設定され、アプリまで再デプロイされたのには驚きました。アプリのfly.toml設定ファイルがあるディレクトリ内で実行したためなのですが、この挙動は予想外で、あまりにも自動的すぎると感じました。なにしろ、ローカル開発用のテストバケットを作っていただけなのですから。

これはドキュメントに記載されていますので、読んでいなかった私にも非はあります――とはいえ「ついでに再デプロイします」と追記しておいてほしいところです。

Fly.ioアプリのコンテキスト内――アプリディレクトリ内、あるいは-a yourappを指定して――で以下のコマンドを実行すると、アプリに自動的にシークレットが設定されます。

この機能はダウンタイムを引き起こす可能性もあるので、例えば--update-app--add-secretsのようなコマンドラインオプションでオプトインする形にすべきだと思います。

また、自動的に設定されたシークレットの値を簡単に確認する方法もありません。これはセキュリティ上の意図的な仕様なのですが、そのせいでローカルテスト用の認証情報を取得するために、(アプリディレクトリの外で)テストバケットを作り直す必要がありました。

$ cd ..  # change out of app directory
$ fly storage create --public --org ben-hoyt --name test-gifty-registry-images2
Your Tigris project (test-gifty-registry-images2) is ready. See details and
next steps with: https://fly.io/docs/reference/tigris/

Set one or more of the following secrets on your target app.
AWS_REGION: auto
BUCKET_NAME: test-gifty-registry-images2
AWS_ENDPOINT_URL_S3: https://fly.storage.tigris.dev
AWS_ACCESS_KEY_ID: ***hidden***
AWS_SECRET_ACCESS_KEY: ***hidden***

これでだいぶマシになりました。そのアクセスキーとシークレットでローカルのアプリ設定を更新したところ、…ほぼ問題なく動きました。まずは以下で説明するいくつかの細かな問題を解消する必要がありましたが。

バケット作成時のもう一つの細かな挙動として――もしfly CLIの開発者の方が読んでいたら――fly storage createを実行した際に表示される環境変数の順序が不定なことが挙げられます。上記の例を見てもわかる通り、1つ目の例ではBUCKET_NAMEが最初に表示されているのに、2つ目ではAWS_REGIONが先頭に来ています。実害はありませんが、少し奇妙で、同じキーが表示されているのだと気づくまで二度見してしまいました。

エンドポイントの変更

デフォルトのAWSのものではなく、新しいTigrisのAPIエンドポイントを使う必要があるのは、おそらく自明でしょう。S3クライアントを作成する際、これまでは次のようにしていました。

cfg := aws.NewConfig().WithCredentials(creds).WithRegion(region)

正しいエンドポイントを使うために、以下の数行を追加しました。

endpoint := os.Getenv("GIFTY_S3_BACKUP_ENDPOINT")
if endpoint != "" {
	cfg = cfg.WithEndpoint(endpoint)
}

なお、私はまだGo用のAWS SDK v1を使っています。v2ではこのあたりのやり方が少し異なります

キャッシュの違い

Amazon S3を使っていたときは、ユーザーがウェディングレジストリ用に新しい画像をアップロードしても、同じファイル名(キー)にそのままアップロードすれば済んでいました。ページを再読み込みして画像が再取得されると、新しい画像が表示されたのです。

S3でもこれが良いやり方だったかは微妙です。というのも、当初のS3は結果整合性しかなかったからです。しかし2020年に強力なread-after-write整合性に切り替わったため、PUTの後にファイルをGETすれば、すぐに更新後の内容が取得できるようになりました。

Tigrisを使い始めてから画像を再アップロードしたところ、新しい画像を表示させるにはCtrl+Shift+R(キャッシュを無視して再読み込み)をする必要がありました。Tigrisはより積極的にキャッシュするようです。Tigrisにアップロードした画像のレスポンスヘッダを見ると、こちらでヘッダを設定していないにもかかわらず、デフォルトでCache-Control: max-age=3600が送られてきます。

これが良いデフォルトかどうかはわかりませんが、ストレージシステムであると同時にCDNでもあろうとしていることを考えれば、その選択も理解できます。これによりコンテンツベースのファイル名のようなキャッシュ手法を使わざるを得なくなり、それ自体は良いことです。

追記:この記事を読んだ後、Tigrisはこの挙動とその意図について説明したキャッシュに関するページを追加してくれました。

いずれにせよ、これはGiftyのアップロード処理を改善する良い機会になりました。ファイル名にコンテンツベースの短いハッシュを付けるようにコードを変更しました。キーとして{registryID}.jpgを使う代わりに{registryID}-{contentHash}.jpgを使うことで、キャッシュの問題は解消されます。

// Add content-based hash to filename to break caching (12 hex digits).
hash := sha1.New()
_, _ = hash.Write(data)
key := fmt.Sprintf("%d-%x.jpg", registry.ID, hash.Sum(nil)[:6])

また、画像の完全なURLについてもamazonaws.comをハードコードするのをやめ、URLフォーマットを設定可能にしました。

// Old version
path := fmt.Sprintf("https://%s.s3-%s.amazonaws.com/%s",
    h.config.S3ImageBucket, h.config.S3ImageRegion, key)

// New version; under Tigris, I set S3ImageURLFormat to
// "https://fly.storage.tigris.dev/gifty-registry-images/%[1]s"
path := fmt.Sprintf(h.config.S3ImageURLFormat, key)

認証の違い

S3を使っていたときは、レジストリ画像用とバックアップ用の2つのバケットがあり、どちらも同じAWSキーとシークレットで認証していました。これはあまり良いやり方ではないかもしれません(バックアップ用には別の認証情報を使うべきだったでしょう)が、私の経験上、小規模なサイトでは単一のキーを使っているのもごく一般的です。

Tigrisでは、作成するバケットごとに新しいキーとシークレットが発行されます。そのため、S3関連の環境変数を2つ追加する軽微なコード変更が必要でした。数行追加するだけで、バックアップも動くようになりました。

ファイルのコピーと「シャドーバケット」

テスト環境で一通り動くようになったので、本番サイトも切り替えることにしました。

S3からの移行用に、Tigrisにはシャドーバケットというクールな機能があります。Tigrisバケットを元のS3バケットに向けておくと、Tigris側にファイルが存在しない場合に「シャドーバケット」からシームレスに取得し(そして今後のリクエストに備えてTigris側にもコピーする)というものです。

この機能を試すべきだったかもしれませんが、使わないことにしました。最も大きいバケットでも数百ファイルしかなく、きれいに切り替えて、完了後はS3への依存を完全に断ちたかったからです。

そこでaws s3 syncコマンドを使ってファイルをS3からローカルディスクにコピーし、そこから新しいTigrisバケットへコピーしました。サイト自体が非常に小規模なので、切り替えの数分間に取りこぼしが出る可能性は極めて低いと判断しました。

新しいTigrisバケット用の認証情報で、gifty-registry-imagesgifty-backupという2つのaws CLIプロファイルを作成しました。デフォルトのプロファイルは引き続きAmazon S3を指しています。

S3からTigrisへコピーするために使ったコマンドは以下の通りです(まずは便利な--dryrunオプションで確認しました)。

# Copy the user-uploaded images
$ mkdir gifty-registry-images
$ cd gifty-registry-images
$ aws s3 sync s3://gifty-registry-images .
...
$ aws s3 sync . s3://gifty-registry-images --profile gifty-registry-images
...

# Copy the backup files
$ cd ..
$ mkdir gifty-backup
$ cd gifty-backup
$ aws s3 sync s3://gifty-backup .
...
$ aws s3 sync . s3://gifty-backup --profile gifty-backup
...

データベースの移行

AWSに支払いを続ける限り、既存のS3のURLは問題なく動き続けます。しかし前述の通り、S3への依存は断ち切りたいと思っていました。

サイトのSQLiteデータベースには画像の完全なURLを保存しています。そこでファイルをコピーした後、シンプルなSQLiteクエリでデータベース内の画像URLを更新しました。

-- First check how many S3 image URLs there are
sqlite> SELECT count(*) FROM registry
        WHERE image_url LIKE 'https://gifty-registry-images.s3%';
389

-- Perform the update
sqlite> UPDATE registry SET image_url = replace(image_url,
        	'https://gifty-registry-images.s3-us-west-1.amazonaws.com/',
        	'https://fly.storage.tigris.dev/gifty-registry-images/')
        WHERE image_url LIKE 'https://gifty-registry-images.s3%';

-- Ensure there are no more S3 URLs
sqlite> SELECT count(*) FROM registry
        WHERE image_url LIKE 'https://gifty-registry-images.s3%';
0

これでサイトは完全にTigrisとFly.ioへ移行し、100% AWSフリーになりました!

最後に

パフォーマンス

古いAWSバケットと新しいTigrisバケットから画像を取得するのにかかる時間を、簡単な非科学的なテストで比較してみました。Tigrisのバケットは「デフォルトでグローバル」なので、最寄りのFly.ioリージョン(シドニー)を利用できます。AWSバケットがあったus-west-1リージョンよりも、ニュージーランドからははるかに近いのです。

予想通り、Tigrisの方がダウンロードはかなり高速でした。小さな画像ではAWSで約1秒かかっていたのがTigrisでは300ミリ秒程度、中くらいの画像ではAWSで約1.3秒のところTigrisでは600ミリ秒程度でした。

料金

現時点でのGiftyのAWS請求額――もはやS3の利用分だけですが――は月に数セントです。私が支払っている金額よりも、管理の手間の方がAWS側のコストとしては大きいでしょう。なので料金は私にとって大きな懸念ではありませんでしたが、念のため、Tigrisの料金はS3と似たようなものです。

  • Tigris: ストレージは1GBあたり月0.02ドル、GETリクエストは1000回あたり0.0005ドルです。
  • S3: ストレージは1GBあたり月0.023ドル、GETリクエストは1000回あたり0.0004ドルです。

いずれにせよ、Tigrisはベータ登録の特典として150ドルの無料クレジットをくれました。これは7500GB・月に相当し、GETリクエストなら3億回分です。私の小さなサイトが一生のうちに使い切ることはまずないでしょう。

免責事項と耐久性

知っておいていただきたいことが一つあります。早期アクセスベータに申し込んだ際、Tigrisから「ベータ期間中は本番ワークロードでの利用は避けてください!」というメールが届きました。

これが単に保身のための免責事項なのか、それとも「ファイルがランダムに消えたり、ベータ期間中は毎晩データを削除します」といった意味なのか尋ねてみました。彼ら曰く、どちらかといえば前者で、Fly.ioもTigrisもこれを本番プラットフォームとして扱っているとのことでした。もちろんリスクはありますが、私はそれで納得し、彼らも間もなくベータを抜ける予定です。

また、オブジェクトの耐久性99.999999999%を謳うS3と比べて、Tigrisの耐久性はどうなのかという点も気になりました。Tigrisはアーキテクチャについて確かに深く考え抜かれているようですが、その耐久性を測定してS3の驚異的な数値と比較することは可能なのでしょうか?そもそもS3の「11個の9」の数値は本当に意味があるのでしょうか?

おわりに

とはいえ、今回の乗り換えは非常に良い経験で、今のところ何の問題も起きていません――もちろん、本当の評価はこれからですが。TigrisとFly.ioの両方が今後も順調に成長し、大手クラウド企業に対して必要とされている競争をもたらしてくれることを願っています。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント