ユースケースによって分割し、データのコード体系と構造で統合する

自分が何らかのソフトウェアシステムを考える際に意識していることを平易に、直感的に書き起こしておく試み。

何を当たり前のことを、と思われたなら成功。

やりたいこととシステムの立ち位置

本来システムはユーザーと目的によって最適解が異なる

目的の達成をシステムは支援する。

たまたま同じデータを利用すると同じシステムを解としたくなる

  • 例えばライン工のように何らかのデータが流れてきて決められた入力や処理をすればいいだけであれば問題ないかもしれない
  • よくあるケースとしては役割が異なればそのデータに対する目的も異なるので、上手に作らないと無理が生じる

ダメな例

こういうものはだいたい、最終的に利用したい成果物を扱う人が用意する。

  • 最も極端な例は巨大な Excel の共同編集。極端な例とは言ったが、実際にはとてもよくあるケースのはず
  • 神Excel もこの例。これはデータで活用する気はなく、紙で提出されていればよいことから、まずその書類を作る業務に最も近かった人が、手元の OS、手元の Excel、手元のプリンタでテンプレートとなるファイルを用意し、その運用を(悪気なく)押し付けてしまう1
  • 大統一フォーマット。最終的な成果物を扱う人はデータのフォーマットが整っていないと困る。しかしそのまま全ユーザーに使ってもらおうとした結果、例えばある人はある項目の入力の際に100ある選択肢から特定の3つしか使いません、みたいなものができあがる。これの積み重ねは単純にストレスでしかない。

これらは適切に分割されておらず、関係者全員が同じものを使っているため、当然変更すると関係者全員に影響するのでおいそれと変更できなくなってしまう

短期で終了する作業のためのものならよいが、長期的な運用を始めてしまうと地獄の始まり。

やりたいこととUIの不満

行う作業によってUIの最適解は異なる

以下は目的と求められることの例。

やりたいこと大切にしたいこと
入力ひたすら高速に入力したい、チェックも速く
進捗確認全体の件数、入力の進み具合を確認したい
レポート
(いわゆるBIの領域)
個々の入力の具合ではなく、統計データだけが欲しい

例えば巨大 Excel で実現するとやたらあちこちスクロールしないといけなくなったり、入力する場所に気を使ったり、様々な制約が生まれる。

これを毎日毎回使わされる側はたまったものではない。ミスしやすいし、ミスったらみんなが使うデータを壊す可能性もある。

ノーコードツールで困ること

システムを「開発」せずに実現できるノーコード、ローコードツールが増えている。しかしこれらは以下の点で最初から課題を抱えていることを承知しておいた方がよい。

  • いわゆるノーコードツールは概ね UI の作り込みを省略、簡略化する2ので特に最適化してほしい場合に UI の不満が出やすい
  • データ構造にも無理が出やすいのでエンジニアからも不評を買いやすい
  • 特に多くの種類の業務を一つのフォーマットに落とし込もうとするとどんどん無理が出てくる
    • これはノーコードツールに限らない話だが、上のような分割の視点がない3から起きることであり、こうなると詰みやすい

解決策

データ、コード体系によって分割と統合を行う

  • データの突き合わせが可能なら一つのデータベース、一つのシステムにこだわる必要はない
  • 突き合わせ可能なデータ(コード体系)をどう設計し、どうシステムを分解するかが腕の見せどころ

ただし、これをスマートに実現するためにはデータを中心に、データベースに近い位置での制御が可能であることが望ましい。

全体で大きなシステムだがユースケースごとに適切に最適化できているなら一つのシステムであってもよい。(伝統的な開発ではこのパターンが多そう)

実は、この「データに近い位置で」が苦手なツールの場合、そもそも無理が出やすい。

特定のツール、特定の言語を使えるか使えないかはそこまで重要ではない

ここまでを考えるに当たっては、少なくともどんなプログラミング言語が扱えるかといった点は重要ではない。あえて言うなら SQL DB の考え方は重要。

  • データの可搬性、可用性を上げるのはツール、言語ではなく「コード体系」や「ID」、「データ構造」
    • この知識、スキルは陳腐化しない
  • 「このツールは使いにくいからあのツールの方がいいかも?」は基本的には隣の芝が青く見えているだけ
    • 乗り換えても乗り換えた先で多かれ少なかれ別な苦労が生まれる

Google Apps Script が使えるからとか Python が使えるから、とかそういうところで有利不利はあまり出ない。少なくとも全体の整合性4を保ちつつ最適化できる余地を残す、みたいな設計力と特定の言語の利用経験はあまり関係がないと思う。たまたま目の前にあるのが Microsoft Office だから、Google Workspace だから、以上には有利にはならないと思う。

もちろんこれらのライセンス契約の領域に踏み込む場合は単にコードが書けるとかツールが使える、だけでは済まない交渉になったりするわけだけど、データとユースケースを中心に考えたらツールの持ち替えや追加がベスト、という判断は十分あり得る。

言語、ツールに必要以上に縛られず、選択肢を増やせる方が重要。もっとも、本当に選択肢を増やすためにはそれなりに経験を積まないといけないけど。

これから開発を専門としない人がシステム、データに関わるために

はじめよう!プロセス設計 ~要件定義のその前に | プログラミング・システム開発,開発技法・ソフトウェアテスト・UML | Gihyo Direct

が参考になる。

  • なぜそのデータが生まれるのか?
  • このプロセスはどこに繋がるのか?

この視点はとても重要で、特定の製品を知っている知っていないよりもはるかに寿命の長いスキルになる。

ただし、実際には自分でゼロベースでデータを中心としたシステムの構築経験なしにこの勘所だけを掴もうとするのはかなり無理があると思う。残念ながら。

特定のツールを導入して解決できるのは恐らくその時点で瞬間的に見える範囲だけであり、何らかのシステムを導入したらその瞬間から新たな課題が生まれ始めるだろう。

現実ってのはそういうものだ。

  1. あくまで本人は利用環境の多様さを理解していないだけで悪気はない 

  2. UIの作り込みはとても手間が掛かるし、最近はデータの型によって欲しいUIは大まかには決まるので、省略させやすいと思ってしまう 

  3. 「データ」の可搬性についての理解が足りない 

  4. 整合性にも種類があることを知っていることも重要。すべてがリアルタイムに解決できるとは限らない。 

Cloud Load Balancingを使い、Cloud RunサービスにIAPで認証を追加する

今回やりたかったのは Cloud IAP という Google がいい具合に処理してくれる認証用の proxy サーバを Cloud Run の前段に置き、特定の人しかアクセスできないようにすること。

Cloud Run には直接 IAP を設定することができず、必要な準備がいくつかある。今回はこの実現のために必要な準備を確認した。

Cloud IAP, Cloud Load Balancingをおさらい

Cloud Run は Cloud Functions 同様、そもそも認証情報付きでしかアクセスできないようにすることもできるが、Cloud IAP ( Indentity-Aware Proxy ) はアカウント単位で許可する人を登録したり、Google Workspace と組み合わせてドメイン丸ごとに対して設定することで、社内限定公開のようなものをお手軽に実現できるスグレモノだ。ただし Cloud IAP を直接設定できるのは App Engine ( GAE ) だけで、それ以外は Cloud Load Balancing のアプリケーションロードバランサと組み合わせる必要がある。

Cloud IAP を通じて Cloud Run へのアクセスを保護する構成

Cloud Load Balancing には L7 アプリケーションロードバランサ(外部/内部)、L4 ネットワークロードバランサ(外部/内部、プロキシ/パススルー)があるが、今回利用するのは外部アプリケーションロードバランサになる。VPN を使わず、インターネット越しのアクセスに対して特定の人のみアクセスできるようにしたい。

この辺は Load Balancing 側のドキュメントではなく IAP 側のドキュメントから参照していくと寄り道が減って分かりやすい。

外部 HTTP(S) ロードバランサの設定  |  Identity-Aware Proxy  |  Google Cloud

※ 意識が発散しやすい人は各ロードバランサの役割の違いなどが気になるだろうが、グッと我慢しないと帰ってこれないので注意。

まとめ

Cloud Run はよくできてる。欲しいものは service の設定画面からポチポチでだいたい追加できそうな勢い。App Engine と違ってサービス単位で Logging などへの遷移がしやすいようになっているし、とても気が利いている。

IAP を利用するための Load Balancing の設定も 2024-02 時点では preview だが、Integration からサクッと設定できてしまう。

確認したこと

  1. Cloud Run の Integration ( preview ) 機能で Custom domains - Google Cloud Load Balancing を追加
  2. その後別途手作業で Load Balancing ( Frontend + Backend ) + Serverless NEG

両方試した。2 は 加えて Frontend に

  • Custom Domain
  • Google-managed Certificate

を設定し、Backend に

  • Identity-Aware Proxy

を設定した。いずれも手作業であり、自動化はしていない。

分かったこと

  • IAP は Load Balancer の Backend service 単位に設定できる。これは GAE に対する IAP の設定の様子と似ている。GAE は service を複数持つことができ、service ごとに IAP の設定を分割できるが、それと同じと考えることができそうだ。Load Balancer が前に立つので GAE に限定されない。
    • ただし Cloud Storage に対しては IAP をセットできないので静的サイトのアクセス制限は GAE の static_files の設定を利用するなど工夫が必要
  • URL map は host も path もなんでもこい
    • 逆に他の Web サービスのように独自の名前を持たず、ユーザーとの間の単一エンドポイントを提供するための IP アドレスの払い出ししかしないので、ドメインは最初に自分たちで確保が必要(これがネックになるなら Cloud Run などではなく GAE でまかなえないか考えるのが吉)
  • Cloud Run 側の ingress を絞らないと誰でもアクセスできて意味がない
  • 固定 IP が振り出されるので IP アドレス代も掛かってるはず

cf.

思ったこと

  • Serveless NEG だけ作る方法はないのか? (Load Balancing 側の設定項目のように見える)
  • なんで Network Endpoint Group と呼ぶのに group 設定ができるように見えないんだ?

試していないこと

  • 異なるプロジェクトにまたがる Backend を設定すること

低頻度アクセスサイトのworkerプロセスのコスパが悪いよ問題にSolidQueueが使えるかも

背景

ActiveJob の backend は意外と悩ましい。

ActiveJob は本来 Rails 専用でも ActiveRecord 専用でもないはずだが、いざ adapter を選んでみると Rails + PostgreSQL 前提です、みたいなことが意外にあったりする。また人気のバックエンドだった Redis も近年はクラウドベンダー側と折り合いが悪かったり、意外と扱いやすくない。1 特に Rails 5, 6 が Webpacker をはじめゴテゴテしつつも変化の早い時期で、別にこれくらいなら Rails じゃなくても Hanami でも Sinatra でもいいんじゃないの?と思っていた頃は特に悩ましかった。

2024-02 現在、Rails 7 は十分シンプルになり、素の cold start の遅さがいわゆるサーバレスとイマイチ相性が悪いが、それ以外は概ね Rails でよいかもと思う状況に戻りつつある。

ActiveJobバックエンド選択時の課題

上記のような背景を踏まえ、現時点で自分が感じている ActiveJob 選択時の課題は以下のように整理できる。

  1. Redis 以外の安定したバックエンドが使えないか
    • Resid のホスティングは以前ほどお手軽価格ではなくなっている。特にメガクラウドに寄せようと思うと、中小規模にとってはだいぶコスパが悪い状態
  2. ActiveRecord を使うのであれば PostgreSQL, MySQL など特定の DB にはできれば依存したくない(細かいバージョンの違いなどはあまり気にしたくない)
  3. できれば Worker dyno, Worker コンテナを使わずに済むならその方がありがたい
  4. クラウド独自のツールは development 環境の再現で手間取るので、なんかもっと楽なものはないか

えーと簡単に言うと手間もお金も掛からず ActiveRecord 任せにできるとそれが最高だな、ですね😁

低頻度アクセスのサイトのバックグラウンドジョブの課題

特に上記 3, 4 が課題になってくるのが低頻度アクセスのサイトである。簡単に言うと

めったにジョブを処理しないのにサーバ(コンテナ)2つ分、ずっとリソースをキープしておくのもったいなくない?

という話だ。

基本的に ActiveJob でバックグラウンドジョブを処理する場合、ユーザーからのアクセスを受け付けるプロセスとは別にバックグラウンドジョブ用のプロセス(コンテナ)を用意して、ジョブ用のプロセス(コンテナ)で polling して処理する方法が一般的なのだが、こうなるとめったにアクセスがないのに24時間起きてる人を最低2人用意しなきゃいけない、という話になり、「なんかコスパ悪くない?」という状態になる。2

もちろんジョブの負荷自体がメモリ使用量的にも実行時間的にも軽いものであれば、全部 inline で処理してしまうというのも一つの手ではある。ちゃんと計測してリソースが足りるのであれば問題ないと思う。しかし現実には

  • 頻度は高くないがコストは一定以上大きい

ケースはままあるし、例えば簡単なメール1通送るだけだったはずの仕組みが、関係各所に送ることになりましたとか、コストの嵩むことを往々にしてやりたくなる。そうなると inline や async adapter で処理するのはインフラのリソースの制限上難しくなってくる3

いちばん確実な解決方法は 4 である。Google Cloud で言うところの Cloud Task を利用する4。これは完全にサーバレスで従量課金なので、バックグラウンドジョブが発生しない限り余計なコストは発生しない。

ただ今度は手元で完全に同じ環境を再現するのが難しいという新たな課題が生まれる。

そんなあなたにSolidQueue

SolidQueue は 37 Signals が新たに開発した ActiveRecord を利用した ActiveJob adapter.5

で、なぜ SolidQueue かと言うと SolidQueue には puma plugin があるので、SolidQueue 用に別途プロセス(コンテナ)を用意しなくても puma の worker の一つとして SolidQueue を管理させることができるから。

もちろんメモリ使用量や実行時間がシビアな場合に気にしなければいけないことは変わらないが、少なくとも揮発しないストレージに情報を残しつつ処理できるので、ジョブを細分化するジョブを作って緩和しつつ、万一メモリ不足でプロセスが異常終了するようなことがあってもジョブが失われないという安心感がある。

簡単に試してみた

wtnabe/example-rails7-sqlite3-solid-queue

上のリポジトリは Cloud Run で FUSE を使って SQLite を読み書きするというリポジトリなのでノイズが多いが、キモは

Procfile.dev の

web: mkdir -p storage/sqlite3 && ./bin/rails s

と ( worker なし ) puma の設定の

plugin :solid_queue

だけで6、solid_queue が polling してバックグラウンドジョブを処理できることである。

※ 今回は実験用に SQLite3 で動かしているが、どうも時々エラーが起きたりするので ActiveRecord-backed とは言っているが、基本的には PostgreSQL or MySQL が前提になっているのだと思われる。

メモ

  • 設定が Rails 本体側というか configure 側に断りなしに漏れていて、solid_queue.yml には一部の設定しか書けないのはちゃんとドキュメントにしてほしい
  • 一応 development では SQLite3 でも動くので手元でサクッと動かす分にはよさそう(InlineやThreadだと何らかの外部ストレージを経由する場合と挙動が変わってしまうので)
  • Rails 7 + SQLite 3 + FUSE ( Cloud Storage ) は基本的な機能だけなら動く
  • 同環境だと SolidQueue の動作の何かが FUSE の制限を踏み抜いてしまうのか、必ず SQLite3::BusyException: database is locked (ActiveRecord::StatementInvalid) か SQLite3::CorruptException: database disk image is malformed (ActiveRecord::StatementInvalid) で死ぬので Cloud SQL でないと無理かも
  1. メガクラウドは結局生のサーバを1台使う、みたいな感じになったりしてまったくお手頃価格にならなかったりする。Heroku や Redis 専門の低価格サービスと組み合わせるといった方法はあるが、今度は管理が煩雑になったりする。 

  2. オンプレならリソースさえ足りれば特に問題はないが、そうなると今度はインフラ管理のコストが乗ってくる 

  3. メモリ使用量の制限やWebプロセスの実行時間制限など 

  4. 実際には特定の routing の Web プロセスにジョブを投げる設定の場合、Web プロセスの実行時間制限に引っかかる可能性は残る。 

  5. 実際には恐らく PostgreSQL と MySQL の該当バージョン以降のもの以外は満足に動作検証されていないと思う。 

  6. リポジトリのものはコメントアウトされてしまっているので戻す必要アリ 

Cloud Run + Cloud Storage FUSE試してみた

Cloud Storage FUSE  |  Google Cloud

Cloud Storage FUSEとは

FUSE が何か分かっている人は Cloud Storage FUSE という名前だけでピンと来ると思うが、よく分からない人のために乱暴に言うと

アプリケーションから見えるファイルシステムの中に Cloud Storage を mount できるもの

になる。図にするとこんな感じ。

※ もちろん本物のファイルシステムとまったく同じにはならないが。

Cloud Storage FUSE  |  Google Cloud

これまでのオブジェクトストレージを読み書きするコードの課題

Cloud Storage に限らずオブジェクトストレージは容量を気にする必要がないし、自動でレプリケーションされたり、とても便利な反面、通常はファイルシステムはファイルシステム、オブジェクトストレージはオブジェクトストレージで、それぞれの I/O にそれぞれ異なるコードが必要になる。

ではオブジェクトストレージの読み書きの動作をどう実現してどう確認するかというと、以下のような工夫が必要だった。

  1. emulator を使う(emulator が用意されていて API Client が emulator に対応していれば簡単)
  2. API Client を直接利用せず、いい具合に I/O を pluggable に切り替えてくれるライブラリを利用する
  3. 実際のクラウド環境にコードを deploy する

3 は間違いがないが、何か試すたびにいちいち deploy しないといけないし、クラウドの利用料金が掛かる(微々たるものかもしれないが)。

つまりオブジェクトストレージ(というかクラウドネイティブの機能)の I/O が入ると testability が落ちてしまうという問題を抱えている。

個人的には上の選択肢の 2 を必ず利用するようにしてきた。(自分で書き始めたコードでない場合は 1 もあり得る。)オブジェクトストレージの読み書きのライブラリの選定の際に差し替え可能で安定していそうなものを選ぶところから始め、読み書きの差し替えを行うコードを挟むところから始めるようにしている。

正直面倒ではあるけど、その後何回も結果を確認する際にいちいちクラウド環境で動かさないといけない方が面倒なので仕方がない。

オブジェクトストレージをmountするとはどういうことか

これがオブジェクトストレージをファイルシステムの一部に mount できるということは、インフラ側の設定さえ済めば、アプリケーションコード側からはファイルの読み書きのコードを書けばそれがローカルのファイルシステムに対するものか FUSE を通じたオブジェクトストレージに対するものかは気にする必要がないということを意味する。

ローカルの開発環境で動いている場合はローカルのファイルシステムを読み書きするし、クラウド環境で動いている場合はクラウドのオブジェクトストレージを読み書きしてくれる。

もちろん全部自分で作っているのであればファイルシステムとオブジェクトストレージの切り替えくらい自分でやれば済む、という場合もあると思うが、例えば DBM 的なライブラリを使いたいとか、利用するツールが暗黙のうちにファイルシステムに依存している場合はそうもいかない。以下の GCP 公式の対応状況では Cloud Storage FUSE 自体が AI 向けに売り出しているように見えるが、これにはそういう事情もあるのかもしれない。

GCPでの公式の状況

2024-02-11 時点で

Cloud Storage FUSEとの統合をサポートしている実行環境として挙げられているのは

  • GKE
  • Vertex AI
  • Deep Learning VM Images
  • Deep Learning Containers
  • Batch jobs
  • Cloud Composer

Cloud Storage FUSE と Google Cloud プロダクトの統合

になっているが、Cloud Run 側のドキュメントには

Cloud Storage FUSE と Google Cloud プロダクトの統合

あれもこれも知っていれば使えるよ、という情報がある。

Cloud Runでの利用は実はめちゃくちゃ簡単になっている

公式の GA の状況はまだまだっぽく見えるが、実際には

Cloud Run と Cloud Storage FUSE (GCS FUSE) の基本

にあるように、gcloud SDK では alpha components という扱いだが、

  • --add-volume
  • --add-volume-mount

フラグを利用することで gen2 実行環境1においてはめちゃくちゃ簡単に deploy できる

gcloud alpha run deploy  |  Google Cloud CLI Documentation

し、Cloud Run のコンソール画面では普通に Cloud Storage バケットを Volume に追加する機能がある。

Docker で例えるとホスト OS 側のパスを指定しておくことで Docker 環境からホスト OS のコードを読み込み、ストレージに書き込めるような、あんな感じで利用できる。

自分の感覚では

生のサーバで cron で動作させて、成果や認証の情報なんかをちょろっとファイルに書き出して終わる、みたいな伝統的なツールを Cloud Run に持ってくるとか簡単にできそう

という状態にきている。ようやく VPS を完全に解約してオッケーかも、と考えている。

やってみたこと

以下を組み合わせて試してみた。

  • 実行環境
    • Cloud Run Service gen2
    • Cloud Run Jobs ( gen2 )
  • 試したツール
    • 生 Ruby
    • Rails
    • GDBM
    • RocksDB
    • SQLite

データの整合性のレベルで確認したわけではないし、並列度も全然厳密にテストしていないが、動作するという意味ではどれも問題なく動かすことができた。唯一ダメだったのは

FUSE 上の Cloud Storage に置いた SQLite を利用して Rails で SolidQueue を puma 管理下で動かすこと

だった。これは起動すらしなかった。

development 環境でもたまに SolidQueue worker プロセスがエラーを吐いて再起動するという現象は起きていたので、SQLite + SolidQueue 自体がそこまで実績がなさそうなのと、それをさらに FUSE の制限上で動かそうとするのが無理だったのかと予想している。

使ったコードは以下。

Rails については SolidQueue 云々より FUSE + SQLite というだけで cold start に 15s くらい掛かるので、これはちょっとさすがになんか別な方法考えた方がいいかも。

RocksDB についてはそこそこのサイズのデータベースを作ってもある程度(64MBとか)のサイズの chunk に分割されるので、key-value ストアとして Cloud Run のリソースの制限 ( 512 MB ) 内で利用するにはなかなかよいかも、と思った。当然速さは出ないけど、それでも十分機能します、みたいなシーンはそれなりにありそう。

  1. 2023年に GA になった Cloud Run Jobs は最初から gen2 で、従来からある service の方はオプション –execution-environment gen2 を与えると gen2 になる。 

Cloud Native Buildpacksのextension完全に理解した

Cloud Native Buildpacksでextensionを諦めつつrun-imageを拡張する (2023-12-31) | あーありがち の続き。

確認環境

2024-01-28 時点

  • macOS ( arm64 )
  • colima 0.6.7
  • Docker
    • Client 25.0.0
    • Server 24.0.7
  • pack 0.32.1 および main ブランチ HEAD
  • gcr.io/buildpacks/builder:google-22 Builder
  • Google Cloud Run

で確認。

この時点の pack CLI が suggest してくる trusted builder はどうも –extension オプションを受け取って意図通りに動作することはなさそうだった。

やりたかったこと / 実現したこと

Cloud Native Buildpacks の build image, run image に用意されていないパッケージを独自に追加し、それを利用する rubygems をインストールし、クラウドインフラ(Cloud Run)上で実行する。

image の拡張の部分は具体的には

  • build image に対しては pack コマンドの –pre-buildpack オプションと –extension オプションを用いてパッケージの追加を行う
  • run image に対しては 前回 同様、事前に build しておいた image を –run-image で与える

方法になった。この辺り何をしたかは 成果物 の sh script にまとめてあるので参考になれば幸い。

目指さなかったこと

オリジナル Builder の作成。

分かったこと

ゼロから Builder を作らなくてもなんとか既存の image を拡張して必要な機能を追加することができる。

一方でそのためにはある程度以上の Docker の知識と Cloud Native Buildpacks の知識が必要になる

特に

  • Dockerfile を自分で書ける
  • Builder, Buildpack, Extension に対する理解が整理できている
  • docker build 中の USER と Cloud Native Buildpacks の USER について理解できている

辺りは必須になる。「Dockerfile を書かずに Docker image を作れる」という謳い文句からは急に遠くなり、正直言うと少々ハードルが高い気がする。Buildpack という言葉がすでに長い歴史を持ち、調べ方によっては正しい情報に辿り着くことも難しい。

こうなると逆にゼロから Builder を作る経験はあった方がよいかもしれない1し、むしろ Docker と配布 Docker image に対する知識、multi-stage build に対する知識があるなら素直に自分で Dockerfile を書いた方が楽かもしれない。

Cloud Native Buildpacksを導入しやすいパターン

  • development 環境の再現に際し docker compose を前提としない
    • ツールのインストール、複数バージョンの切り替え以外については Procfile でプロセスを管理すれば問題ない
  • 開発開始時に最終的な実行環境に必要な Docker 周りの準備を決めきっておく必要がない
    • 特殊な拡張が必要かどうかの決断は先送りしてよい

いわゆる普通の Web アプリだと比較的この構成に近い気がする。どうしても開発機の OS が混ざるのでコンテナに閉じ込めておきたいということでなければ、それほど導入は苦しくないように思う。

extensionを利用してまで既存のbuilderを利用するメリット

  • 開発者がインフラに詳しくなくてもまずはそのまま動くものを作ってもらうことができる
  • docker image の選定などは先送りにできる
  • ある程度以上の期間使うようになると Docker image のメンテナンスはそれなりに面倒になるので、クラウドベンダーの準備してくれる Docker image の上に乗れるのは、長期的には考えることが減って助かる

ハマったこと / 注意点

  • project.toml の schema-version をちゃんと “0.2” にしないと exclude は機能しない。schema-version は optional なので無指定でもエラーにならない(これは正しい動作)が、0.1 で対応していない exclude を指定してもやはりエラーにならない
  • 今のところ自分が動作を確認できた Builder は gcr.io/buildpacks/builder:google-22 のみ。例えば同じ手法で heroku/builder:22 に extension でパッケージを追加することはできなかった。また今後対応 builder が増えるのかどうなのかもよく分からない。(そういうプランを見つけることはできなかった。)
  • Google のリファレンス Configure your build and run images  |  Buildpacks  |  Google Cloud によると builder image はもっとカジュアルに Dockerfile で拡張できそうに見えるのだが、実際に試すとできあがった builder image は “could not parse reference” と言われて使えない

考えると schema-version に限らず、Buildpack API などのバージョン情報も意味がよく分からない。どのバージョンにどのような schema 情報が紐づくのか、分かりやすい資料がない。これはぜひ改善してほしい。

おまけ - Cloud Native Buildpacksのextensionとは

Cloud Foundry / Heroku 時代の buildpack と違い、登場人物が多くてややこしいが、公式ドキュメントのサンプルのコードを読み、実際に動かしてみての自分の理解は、こんな感じになった。

ちょっと分かりにくいが、

  • Extension は image に何らかの機能、役割を provide するもの
  • Extension も Buildpack のように detect, generate というフェーズで image に改変を加える
    • この際、公式ドキュメントとサンプルコードを読むに build image にも run image にも改変を加えられそうだったのだが、自分が試したところ build image にしか上手く改変を加えることはできなかった
    • run image は 前回と同じ手法 を採用することにした
  • ただし、provide したものを require する Buildpack が必要
  • Buildpack は本来 Builder image に含まれるものだが、pack コマンド 0.32.1 には –pre-buildpack / –post-buildpack というオプションで image 化していない生ファイルの Buildpack を追加することができる
    • 今回は rubygems のインストール前に必要なパッケージを追加したいので –pre-buildpack を利用した

成果物

wtnabe/example-cnbp-builder-extension-and-extended-run-image

参考

  1. 自分は Heroku / Cloud Foundry 時代の古い buildpack の知識を援用してごまかした。 

annoy-rbで初めてのベクトル検索をめちゃくちゃ手軽に試す

背景

ベクトル検索は興味あるけど、ベクトル化するところでいつもつまずく1ので、「いちばん簡単なの緯度経度ちゃうの?」と思って探してみたら実は意外と緯度経度情報をベクトル検索するという話題がない2のでやってみた。

今回の成果

データベースも使わず、embedding などの技法も何も要らない、めちゃくちゃ素朴なベクトル検索ができた。

ベクトル検索に関する各要素についてなんとなくの理解だったが、この素朴なものができたおかげで理解がよりクリアになった。(理解したとは言っていない)

おさらい

準備

今回は例によって Ruby でやるので、Ruby 用の binding を探すと以下のような感じ。

AI 関係調べるとよく出てくる安定の ankane さんと洋食さんなので安心でしょう。

ベクトルの類似度、近似度についてイチから学習する気は今回はないので、どっちも軽く触ってみて、なんとなく分かりやすそうな annoy の方を使ってみる。インデックス方式の違いがよく分からん。

作ったもの

  • 日本の各都道府県の人口上位3都市とその緯度経度(たぶん普通は庁舎のある地点だとオム)を ChatGPT 3.5 さんで適当に生成
  • その ID と緯度経度情報(ベクトル)だけを annoy-rb に与えてビルド
  • 緯度経度を与えたらそれを検索するだけの CLI アプリ

wtnabe/example-simple-vector-search-with-annoy-rb

分かったこと

  • annoy-rb は DBM っぽい何かのように使える
  • ベクトルの次元と距離計算の方法を先に決める必要がある
  • データを入れたらビルドが必要
  • ベクトル検索はベクトルしか持っていないので ID で元データと突き合わせが必要
    • 逆に言うと突き合わせさえできるなら別に専用の大掛かりなベクトル検索の仕組みがなくてもベクトル化、embedding さえなんとかなれば、意外と導入はローコストにできるのかも3
  • 単純に緯度経度で検索する場合、コサイン類似度などではなく、ユークリッド距離で比較しないと直感に反した結果になります(そりゃそうだ)
  1. 興味が発散しすぎて帰ってこれない 

  2. たぶん簡単すぎるんでしょう 

  3. ただし、ファイルシステムベースのものはクラウド環境とあまり相性がよくないので、そこは考える必要がある 

Cloud Native Buildpacksでextensionを諦めつつrun-imageを拡張する

要望

Cloud Native Buildpacks の Builder が提供するイメージに含まれないツールを使いたい

例えば ImageMagick. いや ImageMagick くらい入っているか。例えば ffmpeg. さすがに普通入ってなさそう。

今回はそういうツールを使いたいと思った場合の対処方法を調べてみた。

まとめ

  1. 利用したい Builder の Run image の base image に対してDockerfile で追加設定を行い、docker build
  2. pack build --run-image <1でできたイメージ> でコンテナイメージを作る

Cloud Native BuildpacksのBuild imageとRun image

まず前提として押さえておくべき知識について。

今回考えているのは

実行時に、標準的でないツールを導入したい

ということなので、

Build · Cloud Native Buildpacks

で言うところの app image の中に「標準的でないツール」を導入したい、という要望である。

すると工夫が必要なのは Run imageということになる。

Run imageに変更を加える手順

run imageの拡張については非常にコンパクトだが、以下がまとまっている。

ビルドと実行のイメージを構成する  |  Buildpacks  |  Google Cloud

1. run imageのbase imageからdocker buildで拡張する

  • Builder の提供している run-image 用の base image を FROM に持つ Dockerfile を書いて、そこに必要な変更を加える
  • USER の切り替えに注意。例えば Heroku Builder を使う場合は USER root で変更を加えたあと、USER heroku に戻す必要があった。
  • run-image の base は Builder ごとに違うのでそれぞれで確認すること

で、docker build を実行する。

例えば Heroku Builder の Heroku-22 イメージに対して ffmpeg を追加したい場合は以下のような Dockerfile になる

FROM heroku/heroku:22-cnb

USER root

RUN apt-get update && \
  apt-get install -y ffmpeg

USER heroku

2. できたimageをpack build時に利用する

こうゆうこと

$ pack build -B <builder> --run-image <さっき作ったimage>

あれ、結局Dockerfile書いてない?

そうなんです。

ということは想定外の入力が入る可能性のある実行環境でこの run-image を利用する場合は、常にフレッシュで安全な状態を保つように独自に CI/CD で build し続けておく必要があります。

諦めたImage Extension

実は最初

Create an extension · Cloud Native Buildpacks

を参考に CNB image extensions という考え方でなんとかなるんじゃないかと思ったんだけど、全然情報はないし、サンプルのリポジトリを見ても分からないしで、諦めました。

Heroku の Builder を利用したところ、

failed to build: executing lifecycle: builder has an order for extensions which is not supported when using the creator

と言われて、調べてもさっぱり情報が見つからず、Google の Builder はどうも無視してるっぽいので、

今回は諦めです。

Gemfile.lockでRubyのversionを指定する方法

まとめ

Gemfile.lock に利用する Ruby のバージョンをセットする方法

  • Gemfile に ruby でバージョンを指定
  • bundle update --ruby を実行

きっかけ

HerokuのCloud Native Buildpacksを使ってCloud RunでSinatraアプリを動かしてみた (2023-12-16) | あーありがち

で初めて経験したんだけど、なんかどうもこの方面は Gemfile.lock でのバージョン指定は普通っぽいので、整理しておいた。

注意

Google の Buildpack だったか、.ruby-version に改行が含まれてたら Gemfile, Gemfile.lock 側のバージョン指定と矛盾しているという警告が出た。いやもう、そんくらいいい具合に処理してくれよ。なんでみんなそんな繊細なのよ。

そういう意味では、この辺はまだまだ発展途上なのかも。ただあんまりフィードバックしてる人いなさそう(自分も含め)

About

例によって個人のなんちゃらです