GCS データを BigQuery 外部テーブルとして読み込む Terraform 実装例
Terraform で BigQuery(外部テーブル)の作成が以外と簡単だった コードは下記。
GCS にはすでに設定されていることが前提
resource "google_bigquery_dataset" "main" { dataset_id = "<データセット ID>" friendly_name = "名前" description = "This is a test description" location = var.region } resource "google_bigquery_table" "hive_table" { dataset_id = google_bigquery_dataset.main.dataset_id table_id = "<テーブル ID>" external_data_configuration { source_uris = ["gs://<GCS のバケット名>/<パス>/*"] source_format = "PARQUET" autodetect = true hive_partitioning_options { mode = "AUTO" source_uri_prefix = ["gs://<GCS のバケット名>/<パス>"] } } deletion_protection = false }
下記について少し解説すると、フォーマットを指定して、autodetect を true にすると自動でスキーマの定義を取得してくれる。
source_format = "PARQUET" autodetect = true
GCS に、hive 形式でキー(キー=バリュー/キー=バリュー)で保存しておくと便利。
例えば、select * from hogehoge where キー=バリュー で検索できるようになる。
hive_partitioning_options {
mode = "AUTO"
source_uri_prefix = ["gs://<GCS のバケット名>/<パス>"]
}
詳しくは、下記の Terraform GCS プロバイダを参照 https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_dataset https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_table
Podmanでローカルのコンテナレジストリにプッシュする
背景
コンテナを配布する仕組みに興味を持ち、コンテナレジストリをローカルにインストールして遊ぼうと思っていました。
環境
検証した環境は下記です
課題
podmanからコンテンをPushしますが、エラーとなってしまいます。
$ podman push localhost:5000/test:latest Getting image source signatures Error: trying to reuse blob ~ at destination: pinging container registry localhost:5000: Get "https://localhost:5000/v2/": http: server gave HTTP response to HTTPS client
原因
エラーは下記で、httpsで通信してしまうのが原因のようでした。
Error: trying to reuse blob ~ at destination: pinging container registry localhost:5000: Get "https://localhost:5000/v2/": http: server gave HTTP response to HTTPS client
解決案
ここでは、ローカル&検証用なので、HTTPで通信してほしいです。 podman のコマンドヘルプを確認してみると下記の記載を確認できます。デフォルト、http通信でレジストリにアクセスするようなので、falseにしてあげれば通信ができそうです。
podman push -h
...
--tls-verify Require HTTPS and verify certificates when contacting registries (default true)
コマンドの説明の通り --tls-verify=false をつけて実行してみるとPushが成功します。
podman push --tls-verify=false localhost:5000/test:latest Getting image source signatures Copying blob ..... done | Writing manifest to image destination
Pushコマンドではないですが、ドキュメントにも --tls-verify を利用するように案内されていますね。
docs.redhat.com
Postgresのトランザクション分離レベルについての備忘録
はじめに
最近データベース(Postgres)について興味があり時間があれば調べていました。 トランザクション分離レベルについての理解度が浅すぎると思い、手を動かしながら確認することで理解度をあげることができたので備忘録を残します。
特に、今回はSERIALIZABLE の挙動について確認しました。
PostgreSQL: Documentation: 16: 13.2. Transaction Isolation
環境
- docker : postgres:12.17-alpine3.19
SELECT version(); version --------------------------------------------------------------------------------------------------------------- PostgreSQL 12.17 on x86_64-pc-linux-musl, compiled by gcc (Alpine 13.2.1_git20231014) 13.2.1 20231014, 64-bit (1 row)
準備
- テーブルの作成
- 初期データを作成
テーブルの作成
CREATE TABLE my_table ( v INT );
初期データを作成
INSERT INTO my_table (v) VALUES (3); INSERT INTO my_table (v) VALUES (4);
SELECT * FROM my_table; v --- 3 4
確認
ファントムリードやノンリピータブルリードが起きないことを確認
最初に1つトランザクション(A)を開始してSERIALIZABLEにします。SHOWを利用して、トランザクション内の分離レベルを確認できます。
BEGIN; SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; SHOW TRANSACTION ISOLATION LEVEL; transaction_isolation ----------------------- serializable
もう1つトランザクション(B)を開始します
BEGIN;
Bで新しくインサートしてコミットしてデータを確認してみます。5が挿入されています
INSERT INTO my_table (v) VALUES (5); COMMIT; SELECT * FROM my_table; v --- 3 4 5
Aでデータを確認してみると、5は見えていないので、ファントムリードされないことを確認できます。
SELECT * FROM my_table; v --- 3 4
同様にして、Bでデータを書き換えても、Aは更新されていないので、ノンリピータブルリードが起きていないことを確認できます。
UPDATE my_table SET v = 4 WHERE v = 3; COMMIT; SELECT * FROM my_table; v --- 4 5 4
シリアライゼーションアノマリーの問題が起きないか確認
一番大きい値を1件取得して +1 した値をレコードに挿入するとします。
実現するには、下記のようなクエリになると思います。
INSERT INTO my_table (v) SELECT v + 1 FROM my_table ORDER BY v DESC LIMIT 1;
2つ新しくトランザクション(A,B)を開始してどちらも、SERIALIZABLEに設定してデータを挿入します。
Aのトランザクションで6が追加される
BEGIN; SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; INSERT INTO my_table (v) SELECT v + 1 FROM my_table ORDER BY v DESC LIMIT 1; SELECT * FROM my_table; v --- 4 5 4 6
Bのトランザクションでも6が追加される
BEGIN; SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; INSERT INTO my_table (v) SELECT v + 1 FROM my_table ORDER BY v DESC LIMIT 1; SELECT * FROM my_table; v --- 4 5 4 6
Aのトランザクションをコミット
COMMIT; SELECT * FROM my_table; v --- 4 5 4 6
Bのトランザクションでコミット
COMMIT; ERROR: could not serialize access due to read/write dependencies among transactions DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt. HINT: The transaction might succeed if retried.
後(B)でコミットしたトランザクションはエラーとなりロールバックされます。 Bは、先(A)で変更されたトランザクションの状態を反映していないのでエラーとなり、データの挿入を防ぎシリアライゼーションアノマリーの問題を防ぐことができます。
詳細は下記の閲覧をおすすめします。
https://www.postgresql.org/docs/current/transaction-iso.html#XACT-SERIALIZABLE
最後に
全体的に、トランザクション分離レベルについての知識は浅く特に SERIALIZABLE に関してはほぼ何もわかっていませんでした。
手を動かして、動作を確認できて理解をあげることができたと思います。
Vultr Kubernetes Engine に firewall 機能が追加されたのでTerraformで設定してみた
はじめに
VultrのKubernetes(VKE)のクラスタに対してfirewallを設定できるようになっていました。
Terraformにも対応されていたのでTerraformにてfirewallの設定を行ってみました。
VKE Firewall 機能について
VKEのFirewall機能はノード(インスタンス)に対して、Firewallが自動でリンクする機能のようです。
なので、Firewall 機能を有効にしてノードを増やすと設定したFirewallのルールがノードに適用されていました。
※ ドキュメントを見つけれなかったですが、管理画面を見ている限りFirewallはインスタンスに設定されているようでした。
Terraformで設定してみる(管理画面も必要)
Vultrのプロバイダ 2.18 以上で Kubrenetes のArgument Reference のオプションに enable_firewall が追加されます。
したがって enable_firewall = true を設定すると有効化されます。
resource "vultr_kubernetes" "main" { region = <リージョン> version = <バージョン> enable_firewall = true }
しかし、plan してみるとわかるのですが現在(2023/12/21)クラスタが再作成されてしまいます。 新しくクラスタを作る場合は問題ないのですが既存クラスタで設定する場合は不都合がありました。
+ enable_firewall = true # forces replacement
そこでライフサイクルを設定して管理画面から有効化します。
resource "vultr_kubernetes" "main" { region = <リージョン> version = <バージョン> enable_firewall = true lifecycle { ignore_changes = [enable_firewall] } }
terraform plan して apply してみるとステートファイルが書き換わっていると思います。
(planやらapplyやらしていたら、ステートファイルがいつの間にか変わっていたので、多分applyが必要?)
"firewall_group_id": "<firewallのid>",
firewallのルール設定してみる
先ほど設定したFirewallはTerraform内で参照することができるので下記のようにルールを追加することが可能です。
resource "vultr_firewall_rule" "main" { firewall_group_id = vultr_kubernetes.main.firewall_group_id protocol = "tcp" ip_type = "v4" subnet = "0.0.0.0" subnet_size = 0 port = "22" notes = "ssh" }
さいごに
いつの間にかVKEにFirewallを設定できるようになって、インスタンスに対して自動でIP制限を設定できるようになっていました。今までインスタンスが外部に公開されてしまう課題に対しては個別にインスタンスに設定する必要がありましたがアップデートにて自動で設定されるようになり管理が簡単になりました。
FlaskとSQLAlchemyで作るPostgreSQLを使ったシンプルなキューの実装
はじめに
アプリケーションで非同期処理を行う際、RabbitMQのようなキューは大変便利です。
ただし、今回プライベートで作成していたアプリケーションでは、シンプルな機能だけで十分でした(可視性タイムアウトやACKなどの高度な機能は必要ありません)。
また、構成を極力簡素に保ちたいため、別途キュー用のミドルウェアを設置することは避けたいと考えました。
そこで、今回はDBにシンプルなキューの役割を担ってもらうことにしました。
調べてみると、Postgres の機能の一つに SKIP LOCKED という機能がありこれを利用することでキューのようなことを実現できそうです。※1
今回は、Flask + SQLAlchemy + PostgreSQL を利用してシンプルなHTTPベースのキューを実装してみました。
利用するもの
- Python
- Flask
- SQLAlchemy
- PostgreSQL -> バージョン 15.5
ざっくり仕組み ※2
PostgreSQLの機能に SKIP LOCKED という機能があります。 これは、ロックできない行をスキップして取得することができるそうです。 SELECT してロックする際に SKIP LOCKED を利用することで、既にロックされた行を無視して取得できます。
実装
- SQLAlchemyでモデルを定義
- メッセージを登録する処理を追加
- メッセージを取得する処理を追加
- Flaskでエンドポイントを定義
- 確認
SQLAlchemyでモデルを定義
まずはモデルを定義します。
messageカラムにJSONで値を入れることができます。
レコードが増える度にidをインクリメントしていきます。
※ 取得する際はidが大きい値から取得します
from sqlalchemy import Column, Integer, JSON class MQ(Base): __tablename__ = "mq" id = Column(Integer, primary_key=True) message = Column(JSON) created_at = Column(DateTime, default=datetime.utcnow)
メッセージを登録する処理追加
メッセージを登録する処理を add_message() に実装します。 先ほど実装した MQというモデルに、messageを登録します
class Query: def __init__( self, DATABASE_URL: str = "postgresql+psycopg2://postgres:postgres@localhost:5432/tegami", ) -> None: engine = create_engine(DATABASE_URL) Session = sessionmaker(bind=engine) self.session = Session() Base.metadata.create_all(engine) def add_message(self, message: dict): model_message = MQ(message=message) self.session.add(model_message) self.session.commit()
メッセージを取得する処理追加
ロックを取得できるものから最大のIDに対してロックをかける処理を行っています。 SQLAlchemyでは with_for_update(skip_locked=True) を利用することで既にロックされているレコードを無視できます。
def get_message(self):
try:
message = (
self.session.query(MQ)
.order_by(MQ.id.asc())
.with_for_update(skip_locked=True)
.limit(1)
.one()
)
self.session.delete(message)
self.session.commit()
return message.message
except Exception as e:
self.session.rollback()
return None
ちなみに、上記コードが実行された際のSQLは下記でした。
SELECT mq.id AS mq_id, mq.message AS mq_message, mq.created_at AS mq_created_at FROM mq ORDER BY mq.id ASC LIMIT %(param_1)s FOR UPDATE SKIP LOCKED
Flaskでエンドポイント定義
/api/message に対してPOSTすることでキューにメッセージを登録することができます。
さらに /api/messageに対してGETすることでメッセージを取得することができます
@app.route("/api/message", methods=["POST"]) def add_message(): req = request.get_json() query.add_message(message=req) return "OK",200 @app.route("/api/message") def get_message(): message = query.get_message() if message: return message,200 else: return "",501
確認
キューにメッセージを積みたい場合
curl -X POST -H "Content-Type: application/json" -d '{"text":"test"}' http://127.0.0.1:8000/api/message
キューからメッセージを取得したい場合
curl http://127.0.0.1:8000/api/message
最後に
Flask + SQLAlchemy + PostgreSQL を使ってシンプルなキューを実装してみました。
MQモデルやモデルへのクエリ処理を修正することでさらに複雑な処理を実装できそうです。
参考
Fly.ioのコマンド操作:開発時間外のアプリ無効化の手順
はじめに
プライベートな開発で flyio を利用しています。
課題
開発の時間以外は、リクエストできないようにしたいのですが、現在のWebコンソールを見た限りはアプリケーションをいったん削除するしかなさそうでした。
アプリケーションの削除までは行いたくないと思ったので、何か解決方法はないかとドキュメントを読んでると解決できそうな方法があったので紹介です。
解決
コマンドを利用して下記を実行すると、インスタンス(VM)の数を0にすることができました。 実際にアクセスすると、リクエストを受け付けなくなっていました。
flyctl scale count 0
確認のためインスタンスのサイズを確認するコマンドを実行してみると、インスタンスの表示がなくなっていると思います。
flyctl machine list
さらに、開発を再開したいときは下記コマンドでインスタンスを起動できました。
flyctl scale count 1
Svelte(Vite)ビルド時に変数を渡したい
はじめに
普段インフラがメインであまりフロント周りを触る機会がなかったものの、趣味のWebアプリを作るためにフロントエンドのフレームワーク Svelte に入門しました。
課題
コードの動作の中でバックエンドのAPIサーバにアクセスする箇所が出てきました。
普段はローカルで開発するので、開発時のAPI http://localhost ~~ にリクエストしたいのですが、サーバにデプロイする際は https://ドメイン ~~ にアクセスしてほしいものです。
しかし、下記のようにコードにハードコードしてしまうと、開発時とサーバデプロイ時でAPIのエンドポイントを変更する必要があるので手間でした。
const API_ENDPOINT: string = <APIのエンドポイント>
解決
SvelteのビルドはViteというものが行っており、Viteでビルドする際に変数を渡すことができそうです。 ja.vitejs.dev
実際にビルド(npm run build)してみると vite build というのが行われているぽい
> frontend@0.0.1 build > vite build
vite のドキュメントを読んでいると、.envや.env.developmentなど変数を記載しておくことでビルド時に変数を渡すことができます。
例えば下記のように
VITE_API_ENDPOINT="http://localhost:8000"
Svelteのコード内で利用する際は下記のように利用できるので、ビルド時に変数を書き換える必要がなくなりました。
// import.meta.env.VITE_~~ のように VITE_で始まる変数をコード内で利用できる const API_ENDPOINT: string = import.meta.env.VITE_API_ENDPOINT
さらに、
ビルド時に開発時は .env.development を利用して、サーバデプロイ時は .envを利用するように package.json を変更します。
"scripts": { "build-dev": "vite build --mode development", "build": "vite build",
これで、 npm run build-dev 実行時は env.development が利用され、npm run build 実行時は .env の変数が格納されるようにできました。