はじめに
AWS(Amazon Web Services)やGCP(Google Cloud Platform)、AzureなどでもなかなかDBのコストは高いため(AWSのサーバーレスのRDSは安いかもしれません)、利用頻度の低いケースやレスポンスが遅くても大丈夫な場合を前提としたなるべくコストの安いDBの仕組みを作ってみようという記事になります。
まず1回目は、SQLのクエリを受け付ける箇所について作成していこうと思います。また、Google Cloud Platfromで構築していきます。使用するサービスは、もちろんCloud SQLは今回は使用できないのでコストの安そうなサービスを使用していきます。
仕組み
どのようにして低コストの仕組みを作るのか考えてみましたが、作りやすさ(コード量の少なさなど)を重視して次のようなサービスを利用していきます。
- Cloud Functions
- Cloud Storage
- Pub/Sub
処理の流れは次の通りです。
- SQLクエリの受付: Pub/Sub
- Pub/Subに格納されたSQLを実行: Cloud Function
- SQLiteにDBの処理を任せます
- DBで更新された内容はCloud Storageに保存
構築する
まずはPub/Subの準備からはじめます。Google CloudのWebコンソールからPub/Subを選択して新しく「トピック」を作成します。
次にGoogle Cloud StorageでSQLiteのDBのファイルを保存するバケットを作成します。リージョンなどは単一リージョンなどでも大丈夫です(コスト優先で)。
最後にCloud Functionsを作成します。ここが今回のメインです。Cloud Functionsは第2世代が使えるようになっているため、第2世代を今回は選びました。実際にはCloud Runが動いているようです。ここで大事な設定が一つあります。それはPub/Subからメッセージを受け取るためのトリガーの設定です。
作成画面に「EVENTARCのトリガーを追加」というボタンがあるのでそちらを押します。そこでイベントプロバイダーをCloud Pub/Sub、トピックを先ほど作成したPub/Subのトピックを設定します。ここでサービスアカウントを任意なものを設定しておきます。
「次へ」を押すとランタイムの設定やソースコードの入力ができる画面になります。今回ランタイムはなんとなくRuby 3.0を選択しました。app.rbやGemfileは次のようになります。
source "https://rubygems.org"
gem "functions_framework", "~> 0.11"
gem "google-cloud-storage"
gem "sqlite3"
Gemfile.lockもこの内容が反映されていたいものだとうまく起動しないため、ローカルの環境でbundle install
して作成されたファイルをここに張り付けてください。
require "functions_framework"
require "base64"
require "google/cloud/storage"
require "sqlite3"
require "json"
FunctionsFramework.cloud_event "hello_pubsub" do |event|
message = Base64.decode64 event.data["message"]["data"] rescue "SELECT 1"
input = JSON.parse(message)
query_id = input['id']
query = input['query']
storage = Google::Cloud::Storage.new
bucket_name = "{作成したバケット名}"
bucket = storage.bucket(bucket_name, skip_lookup: true)
file_name = 'hello.db'
file = bucket.file(file_name)
file.download(file_name) if file.exists?
db = SQLite3::Database.new(file_name)
if query.downcase.index('select') == 0
rows = []
db.execute(query) do |row|
rows.push(row)
end
rows_json = JSON.generate(rows)
logger.info("output: #{rows_json}")
bucket.create_file(StringIO.new(rows_json), "output/#{query_id}.txt")
else
db.execute(query)
end
db.close
file = bucket.create_file(file_name, file_name)
logger.info("input: #{message}")
end
これでCloud Functionの作成はおしまいです。しかし、このままだとPub/Subからの呼び出しに失敗してしまうためEventarcトリガーに設定しているサービスアカウントのロールを追加します。
「IAMと管理」の画面を開いて、「IAM」のEventracに設定しているサービスアカウントの編集ボタン(プリンシパルを編集します)を押して「Cloud Run起動元」を追加します。
SQLを実行する
今回は手動でメッセージを送信します。Pub/Subのトピックを開いて今回作成したトピックを選び、メッセージのタブを選びます。「メッセージのパブリッシュ」というボタンがあるので、それを押して次の内容のメッセージを入力して、「公開」をボタンを押します。
Jsonの形式で入力するようにしています。queryにSQLを記述します。
{"id":"test_001", "query": "CREATE TABLE users (id int, name varchar(30));"}
{"id":"test_002", "query": "INSERT INTO users VALUES(1, 'test');"}
{"id":"test_003", "query": "SELECT * FROM users;"}
SELECTの場合、今回はCloud Storageに結果を保存するようにしています。この結果の返し方は、いろいろと方法がありそうですね。
おわりに
次回は、クエリを発行する側を作成していこうと思います。実際には、同時書き込みなどの制御などまだまだ課題は多いですが、改良していければと思っています。