
Farm がオープンソースに:手間いらずのプレビュー環境
Farm がオープンソースに:手間いらずのプレビュー環境
Farm が 1 台の VM 上のセルフホスト・スクリプトから Docker と Kubernetes 上のプレビュー環境オーケストレーターへ成長した経緯と、コードを公開した理由。

どのプルリクエストも、テストを通すだけでなく実際に動く姿で見せたいものです。デザイナーにも、マネージャーにも、隣のチームにも見せて、みんなにUIをあれこれつついてもらう。うまく動いて喜んでもらえることもあれば、バグが見つかってがっかりされることもあるでしょう。一見、難しいことは何もなさそうです。環境を立ち上げて、リンクを共有するだけ。ところが、プルリクエストが毎日何十件も舞い込み、チームの数も多いとなると、「ただ環境を立ち上げるだけ」の作業が本格的なインフラプロジェクトへと姿を変えるのです。
こんにちは!Mikhail Golbakhと申します。Yandex Cloudのシニアフロントエンド開発者です。私はチームとともに、もう数年にわたってFarmを開発しています。Farmは、プルリクエストごとにプレビュー環境を立ち上げるサービスです。この記事では、Farmが1台の仮想マシン上のシンプルなセルフホスト型スクリプトから、DockerとKubernetesの上で動くオーケストレーターへと成長した道のりと、最終的にコードをオープンソースとして公開するに至った理由をご紹介します。
Farmの最初のバージョンができるまで
Yandexには多くのフロントエンド開発チームがあり、1日に数百件のプルリクエストが作成されます。プルリクエストごとに、多数のテストやその他のCIチェックが実行されます。ユニットテスト、型チェック、リンターは、隔離された仮想マシン上で問題なく動きます。
しかし、ブランチから完全なアプリケーションを立ち上げる必要があるときはどうすればよいでしょうか。この問いから、フロントエンド開発者が絶えず直面する課題が導かれます。プルリクエストごとの「ベータ環境」の立ち上げです。まず思い浮かぶのは、本番を模した専用のdev環境を1つ用意して、開発者の間で共有するやり方でしょう。初期の小さなチームはよくそうしますが、ほどなくスケーリングの問題に突き当たります。1つの環境ではもう分け合えませんし、そこでe2eテストを回すのも無理があります。その安定性には明らかに疑問符が付くでしょうから。
伝統的に、各チームはdev環境をデプロイするプロセスを自前で整え、その構築にそれなりの労力を、維持にリソースを費やしています。私たちも例外ではなく、独自のソリューションを編み出しました。ただし最初から、インフラの違いがあり得ることを踏まえたうえで、数十のチームにスケールできる共通技術にすることを目指して設計しました。
2018年にさかのぼりますが、Yandex Cloudのフロントエンドチームは、Farmの最初のバージョンを作りました。実体はシンプルなセルフホスト型サービスで、リポジトリのブランチからコードをダウンロードし、対象アプリケーションのビルドコマンドを実行し、そのプロセスをVM上で起動して、専用ドメインからのトラフィックをルーティングするというものでした。コマンドセット自体もハードコードされていました。Farmは何よりもまず自社プロジェクト向けで、それらはNode.js製Webサービス用の共通コンポーネントの上に作られていたからです。こうして出来上がったのは、Yandex風味の効いた、いわば簡易デプロイシステムでした。
年月を重ねるうちに、Farmサービスは進化していきました。プロジェクトごとに別々のビルド設定・起動設定を扱えるようになり、本格的なUIとデータベース、さらにYandexのインフラでの作業を楽にする大量の追加コードを手に入れました。しかしサービスの本質は変わらず、解決するタスクも相変わらず次のとおりでした。
-
プルリクエストからコードを取得してプロジェクトをビルドすること。
-
起動したアプリケーションへトラフィックをルーティングすること。
-
ソリューションを、配布もYandexのCIプロセスへの組み込みも容易な、単一のインフラコンポーネントに統一すること。
動作の仕組み
Farmは小さなNode.jsアプリケーションで、簡略化したワークフローは次のとおりです。
-
Farmが生成リクエストを受け取ります。リクエストはCIのタスクから届くことも、UIのフォームを通じてユーザーから直接届くこともあります。
-
新しいアプリケーションインスタンスがqueuedステータスでデータベースに登録され、ビルドキューに並びます。
-
Farmがキューを順に処理し、対象のリポジトリとブランチからコードをダウンロードし、設定を読み込んでビルドを開始します。
-
アプリケーションのプロセスが起動します。プロセスは
<instance_dir>/dist/server.sockにソケットを開くことを期待されています。 -
Farm、より正確にはここからは専用の設定を持つnginxが、一意のドメインへのトラフィックのルーティングを始めます。たとえばd008838956420e285cee652262dd5d18b2ed2a2a.farm.example.comといった具合です。

図を見るとわかるように、私たちはロードバランシングの責務をFarmには持たせず、nginxを使いました。この方針はこの先も変わりません。トラフィックのプロキシを自前で維持するのは、できれば抱え込みたくない非自明なタスクだからです。加えて、GitとArc(Yandex社内独自のバージョン管理システム)のサポートもありました。
ビルドと起動そのものは、Farmがコマンドを実行するだけの飾り気のない仕組みでした。何もかもが、これ以上ないほど無骨で直球でした。
# ビルド
npm ci
npm run build
# 起動
npm run start
全体像をつかむために、farm.jsonの設定例も見ておきましょう。
{
"preview-generator": {
"build": ["npm ci", "npm run build"],
"start": {
"command": "npm",
"args": "run start"
},
"env": {
"APP_BUILDER_CDN": "false",
"IS_FARM": "true"
},
"instances": [
{
"name": "preprod",
"urlTemplate": "https://{hash}.farm.example.com",
"env": {
"APP_INSTALLATION": "russia",
"APP_ENV": "preprod"
}
},
{
"name": "prod",
"urlTemplate": "https://{hash}.farm.example.com",
"env": {
"APP_INSTALLATION": "russia",
"APP_ENV": "prod"
}
}
]
}
}
こうして出来上がったのは、まさに「頼れる働き者」でした。導入するチームに残された仕事は、仮想マシンを用意し、Farm本体とプロジェクトの設定を記述し、Farmを立ち上げて、プルリクエスト用のCIプロセスを整えることだけ。簡単そうに聞こえますよね?しかし……
成長と新たな課題
Farmはこの状態のままかなり長い間生き続け、与えられた役割をそこそこ立派に果たしていました。当初は「相乗り」構成すらありました。複数のインストール環境を維持する手間を省くため、いくつかのフロントエンドチームで1つのFarmを共用していたのです。しかしチームの規模は大きくなり、数も増えていく一方で、Farmは1台のVMの枠内でしか動けませんでした。やがて、どれほど強力なマシン構成でも足りなくなっていきました。
複数のビルドを並列に走らせるとマシンのリソースを食い尽くし、インスタンスがかなりの時間キューで待たされることもありました。そこで、共用Farmをより小さなFarmへ、ときにはプロジェクト単位にまで分割していく、自然発生的な動きが始まりました。
言うまでもなく、この分割は問題を解決したのではなく、先送りしただけでした。計算リソースは相変わらず尽きていき、新しいチームはFarmのサポートと保守に余分な労力を割く羽目になりました。たとえばVMのディスクはかなりの頻度で満杯になり、不要なファイルを手作業で掃除せざるを得ませんでした。つまり1つ目の問題は、管理もスケーリングもできない限られたリソースです。これを覚えておきましょう。
もう1つの問題は、Farmが内部でデータベースとしてSQLiteを、Knexのような抽象化を一切挟まずに使っていたことです。これはベンダーロックを生み、さらにマイグレーションの仕組みがないせいで、スキーマを変えるたびに全データを消すことを強いられました。Farmがチームごとの多数のインストール環境に分割されていたことも相まって、誰もアップデートしたがらないという事態を招きました。何が壊れるかわかったものではないし、おまけにデータもきっと消す羽目になる。まさに「動いているものは触るな」というやつです。
さらに、FarmはNode.jsアプリケーションしか起動できず、しかもすべてのプロセスは何の隔離もないままホスト上で直接動いていました。隔離がない以上、アプリケーションはトラフィックの受け口としていつものポートを気軽に使うわけにはいきません。Farmはポートの管理をしていなかったからです。そのため通信にはUnixソケットが使われ、アプリケーションはこのやや風変わりな契約に従わされていました。
というわけで、解決すべき問題は次のとおりでした。
-
リソースの制約と、その管理およびスケーリングの欠如。
-
データベースマイグレーションの欠如と、SQLiteへの強い依存。
-
インスタンスの隔離の欠如、そして「Unixソケットを公開するNode.jsアプリケーション」という制約。
-
サポートと運用の負担。そもそも私たちはチームの暮らしを楽にしたかったのであって、頭痛の種を増やしたかったわけではありません(これは追加の問題としていったん脇に置きますが、終盤で改めて戻ってきます)。
課題にどう取り組んだか
経験を積んだエンジニアがこの課題を見れば、十中八九「Farmをk8sクラスタのレールに載せよう」と提案するはずです。私たちもまさにその考えに徐々にたどり着きました。アプリケーションのビルドとデプロイをクラスタの計算資源へ、個別のPodへと移し、自動スケーリングとリソース管理にはクラスタの機能を使うのです。アプリケーション自体は、誰もが見慣れたDockerコンテナとして配布します。これは私たちの業界では非常に明快でシンプルな契約です。そのうえで、小さなチームがk8sソリューションへの骨の折れる移行をせずに済むよう、アプリケーションを従来どおりプロセスとして起動する方式も残しておく必要がありました。
Farmをただ捨てて、CIからk8sクラスタへ直接アプリケーションをデプロイする方式へ乗り換えることは不可能でした。理由は次のとおりです。
-
Farmはすでに、私たちの既存プロセスとCIの隅々にまで深く根を張っていました。チームにまったく新しいソリューションへの移行を強いるのは筋が通りません。
-
細部に踏み込めば、Farmはアプリケーションのビルドと起動だけをしているわけではありません。(nginx経由とはいえ)ルーティングの責任も負い、使われていないインスタンスの停止・削除も行い、そのほかにも多くの仕事をこなしています。
-
さらに、便利なUIの存在も見逃せません。UIのおかげで、フロントエンドだけでなくバックエンドのエンジニアも、さまざまな機能を試すためにアプリケーションのインスタンスを立ち上げられるのです。
実際には、k8sサポートの追加は当初の想定より難航しました。ごく初期のバージョンから、アプリケーションインスタンスを扱うロジックはコードベース中に散らばっており、長年のうちにそこへ無数の細かな仕様と「エレガントなその場しのぎ」が溜め込まれていました。その横にもう1つの実装を押し込むのは骨の折れる仕事でした。
大規模なリファクタリング
最初の一歩として、Farmのコードの大規模なリファクタリングに乗り出しました。まず必要なのは、インスタンスを扱うロジックを専用のプロバイダークラスにカプセル化することでした。プロバイダーとは、いわばFarmのバックエンドで、アプリケーションのビルドとデプロイの戦略を定めるものです。これができれば、当面はレガシー方式とk8sを並走させ、その先は単一のインターフェースを通じて新しい実装を追加していけます。
コードを分解した結果、プロバイダー固有のメソッドをすべて洗い出し、それらを記述する抽象クラスを用意できました。少し簡略化すると、すべてはこのコードに行き着きました。
class BaseFarmProvider {
startup(): Promise<void>;
buildInstance(
generateData: GenerateInstanceData,
observer: SubscriptionObserver<InstanceObservableEmitValue>,
): Promise<void>;
stopBuilder(hash: string): Promise<void>;
startInstance(instance: Instance): Promise<void>;
stopInstance(hash: string): Promise<void>;
restartInstance(instance: Instance): Promise<void>;
deleteInstance(hash: string): Promise<void>;
getInstanceStatus(instance: Instance): Promise<InstanceProviderStatus>;
getInstances(): Promise<Array<InstanceProviderInfo>>;
getInstanceLogs(params: {
hash: string;
stdout?: LogParams;
stderr?: LogParams;
}): Promise<{stdout?: string; stderr?: string}>;
}
ほぼ同じ時期に、データベースへのアクセスもすべて抽象化してKnexへ移行しました。将来的に、より強力で永続性のある別のDBMSへ乗り換えられるようにするためです。おまけに、データベーススキーマの変更を楽にする、安上がりなマイグレーションの仕組みも手に入りました。
Kubernetesを使った新しい構成
新しい構成では、Farmはクラスタ内にデプロイされてコントローラーとして働き、k8s APIに接続してクラスタのリソースを管理し始めます。ビルド用およびアプリケーション用のPodを起動し、そのほかのリソースを作成・削除するのです。
この構成を理解するには、2つの主要なプロセス、つまりアプリケーションインスタンスのビルドとデプロイを押さえれば十分です。
ビルドはいくつかのステージから成ります。
-
ユーザーまたはCIがビルドを開始します(buildInstanceメソッド)。
-
対象アプリケーション用のFarm設定がブランチから取得されます(リポジトリ直下のfarm.jsonファイル)。
-
ビルダーPodが起動します。このPodがブランチからコードをダウンロードし、Dockerイメージ(デフォルトではDockerfile.farm)をビルドして、あらかじめ決められたレジストリへプッシュします。
-
ビルダーPodのログはすべてリアルタイムでストリーミングされ、デバッグ用にUIに表示されます。
-
ビルドが完了すると、FarmはビルダーPodを削除します。

ビルドの出力はアーティファクト、すなわちレジストリにプッシュ済みで、デプロイと起動の準備が整ったアプリケーションイメージです。おまけにDockerによるビルドキャッシュも手に入り、これはCIの高速化に大いに役立っています。
デプロイはビルドの直後、同じbuildInstanceメソッドの中で始まります。イメージがプッシュされた瞬間から続きを追いましょう。
-
クラスタ内にDeploymentが作成されます。ここでは前のステージで得たイメージが使われます。
-
Farm設定で指定したポートを持つServiceと、インスタンスIDに基づくドメインを持つIngressが作成されます。
-
そしてここで新たなプレイヤー、Ingress NGINX Controllerが登場します。ルーティング全般を引き受け、アプリケーションを外部から到達可能にします。ここでもFarmは流儀を貫き、トラフィックのプロキシをシステムの別のコンポーネントに委ねるのです。

技術的には、ルーティングには他のどのIngressコントローラーでも使えます。必要とあらばクラウドのALBの類でも構いません。私たちがIngress NGINX Controllerを選んだのは、扱いのシンプルさと設定反映の速さが理由です。
こうしておおよそ、アクセスして機能を試せる、動くアプリケーションインスタンスが手に入ります。さらにFarmは、リソースが重複しないこと、すべての操作が冪等であることにも気を配ります。これは安定稼働のためにきわめて重要です。
素のDockerによるシンプルな構成
Farmにk8sサポートを加えるアイデアが生まれると、続いてかなり当然の提案が出てきました。インスタンスを、仮想マシン上のDockerでそのまま動かす方式もサポートしてはどうだろう?これならインスタンスの隔離問題が解決し、Node.jsアプリケーション限定という縛りとポートの制約も外れ、Farmにもう1つの動作モードが加わります。今度はk8sクラスタを持たない小さなチームのためのモードです。ゆくゆくは、素のNode.jsプロセスとして起動する古い方式を完全に廃止でき、山ほどのレガシーコードを削除して保守を大幅に簡素化できるはずでした。
結局どう動くのでしょうか?FarmはソケットでDocker Engineに接続し、自らがビルダー役を務めます。ここには独立したビルド用Podはありません。そしてこれがk8s構成との最初の重要な違いです。インスタンスのイメージはローカルの同じデーモン上でビルドされ、そのままそこに住み続けます。レジストリへプッシュする必要はなく、レジストリの出番はせいぜいビルド中にベースイメージを取得するときくらいです。目に見えてシンプルになります。そしてそれこそ、クラスタを持たない小さなチームに必要なものです。
動作モードは2つ用意しました。
-
docker_container — Farm自身が、ホストのソケットをマウントしたコンテナとして動き、同じデーモン上の「隣人」コンテナたちを指揮します。
-
vm — Farmが、自前のDocker Engineを備えたVM上に直接住み着きます。
Farm本体もすべてのインスタンスも共有のDockerネットワーク(デフォルトではfarm)に参加し、各アプリケーションインスタンスにはfarm-docker-<hash>という、見るからにそれとわかる名前のコンテナが割り当てられます。トラフィックを流す段になったら、まさにこの名前でコンテナを探し当てます。
k8s構成と同じく、ビルドと起動は同じbuildInstanceメソッドに同居しています。ただしPodへの分離はありません。
-
ユーザーまたはCIがビルドを開始します(buildInstanceメソッド)。
-
ブランチからコードが取得され、farm.jsonからFarm設定が読み込まれます。ここからDockerfileのパスと、ビルド用・実行用の変数が得られます。
-
FarmがローカルでDockerイメージをビルドして
farm-docker-<hash>というタグを付けます。その間、ビルドログはすべてデバッグ用にUIへストリーミングされます。 -
出来上がったイメージからコンテナが作られ、farmネットワークの中で起動します。環境変数は引き渡され、望めば起動コマンドの上書きもできます。

トラフィックのルーティング
ここでもFarmはプロキシをnginxに委ねます。インスタンスのコンテナはホスト側にポートを一切公開しません。到達するには、farmというDockerネットワークの内側でfarm-docker-<hash>という名前を使います。この名前はDocker組み込みのDNSが解決します。
唯一の問題は、その名前を解決できるnginxがどこに住んでいるかで、これはモードによって変わります。
-
docker_containerモードでは、ホストからのリクエストはFarmのコンテナへ入り、farmネットワーク上にいる内部のnginxが、トラフィックをインスタンスまで直接届けます。
-
vmモードでは、Farmのnginxはホスト上で直接動いていてコンテナ名が見えません。そのためリクエストを独立したコンテナであるDocker Proxy(同じnginxですが、farmネットワークの内側で起動しているもの)へプロキシし、そこから目的のインスタンスへトラフィックが渡されます。

こうして、クラスタの類は一切なしで、Dockerの入った普通の仮想マシン上に動くインスタンスが手に入ります。コードはイメージにビルドされ、イメージはコンテナになり、そこへnginxがトラフィックを届けます。
ついでに、例の風変わりなUnixソケット契約ごと隔離の問題も片付きました。アプリケーションはいまや普通のDockerイメージとして配布され、Farmの舞台裏のことなど何も知らないまま、自分のポートで静かにリッスンするだけです。
Farmの進化とオープンソース化
プロバイダーの抽象化、多様なビルド・起動方式のサポート、Knexへの移行。こうした改善をすべて終えたFarmは、もはやYandex固有の事情が大きな役割を果たさない、成熟した本格的なプロダクトの姿になっていました。Farmは少しずつ、その下で動かす技術に固く縛られないオーケストレーターへ変わっていったのです。
その姿はもう、Heroku風の簡易デプロイシステムをセルフホスト型サービスにしたようなものでした。そこで、より野心的な計画が持ち上がります。Farmのコードを私たちのオープンソースプロジェクトGravity UIの一部として公開し、社内だけでなくコミュニティ全体の役に立てよう、というものです。
しかし、このコード公開という美しいアイデアには1つ落とし穴がありました。Farmはより抽象的でクリーンになったとはいえ、Yandex固有の部分はまだ山ほど残っていたのです。社内認証、Arcのサポート、社内の課題トラッカーへのステータス投稿、テレメトリー、そのほか社内インフラに結びついた細々としたもの。これらを丸ごと公開するのは得策ではありませんし、コミュニティにとってはそもそも無用の長物です。かといって単純に切り落とすわけにもいきません。私たち自身のインストール環境はまさにその固有部分の上で動いており、毎日数十のチームが使っているのですから。
そこで課題はこう整理されました。Yandex固有のものをすべて括弧の外へ出し、Farmから設定可能なコアを切り出す。そして固有部分は、コアのコードに触れることなく外側から差し戻せるようにする。要するに、世界へ公開できるものと社内に残るものとの間に、もう1本の境界線を引く必要があったのです。
最終的に、Farmは物理的に2つのコードベースへ分割されました。
-
オープンなコア。オーケストレーション全体、Dockerとk8sのプロバイダー、Gitサポート、Knexベースのデータベース、API、UI、ビルドキュー、ヘルスチェック。要するに、Farmをプロダクトたらしめるすべてです。コアはYandexについて何も知らないので、そのまま安心して公開できます。
-
その上にプロプライエタリな部分をすべて上乗せする、薄い拡張レイヤー。
両者をつなぐ接着剤がプラグインレジストリです。コアは自力で立ち上がり、単一のエントリーポイントを提供します。拡張レイヤーはそこを通じて、明確に定義されたインターフェースに対して自分の実装を登録するのです。その結果、Yandex部分はいまや丸ごと1つの小さなモジュールに収まり、宣言的に接続されています。
initCoreExtension(async () => {
// 独自のインスタンス起動方式
coreRegistry.farmProviders.plugIn('process', {
constructor: ({internalApi, config}) => new ProcessFarmProvider(internalApi, config),
});
// 独自のバージョン管理システム
coreRegistry.vcs.plugIn('arc', {constructor: () => new ArcVcs()});
// 独自のWebhookアクション(たとえばトラッカーへのステータス投稿)
coreRegistry.webhookActions.plugIn('tracker', new TrackerWebhookAction());
// 独自の認証
coreRegistry.authProviders.plugIn('internal', {constructor: () => new InternalAuthProvider()});
// ...さらにテレメトリー、CSPドメイン、UIのメニュー項目
});
レジストリは、それぞれが固有のインターフェースを持つ独立した拡張ポイントの集合として構成されています。変わり得るものはすべて、これらの拡張ポイントを通じて外に出されています。
-
プロバイダー(farmProviders) — どうやって、どこへデプロイするか。
-
バージョン管理システム(vcs) — 公開のgit、社内のarc。
-
認証(authProviders) — ユーザーをUIとAPIへどう通すか。
-
Webhookアクション(webhookActions) — CIのイベントに応じて何をするか。たとえばトラッカーへのステータスの書き戻しなど。
-
farm.jsonのスキーマ(farmJsonConfig) — 特定のプロバイダーの都合に合わせた、アプリケーション設定の独自フィールド。
-
UIとセキュリティ(uiConfiguration、cspDirectives) — メニュー項目と、CSPにおける信頼済みドメインのリスト。
肝心なのは、コアにとってこれらの実装がすべて対等だという点です。コアから見ればarcはgitと何ら変わらず、processもdockerやk8sと変わりません。だからこそ、社内の舞台裏を気にせずコアを公開でき、一方で私たちのインストール環境は、コアを拡張レイヤーと一緒にビルドするだけで、以前と変わらず動き続けるのです。
Farmをk8sに展開する
長い道のりを経て、私たちはついにFarmのインストール環境をk8sへ移す段階にたどり着きました。選んだ戦略はこうです。まず最大規模のインストール環境をKubernetesのレールへ移行する。うまく動いたら、残りのプロジェクトをローカルのFarmから1つの共用インストール環境へ移し、「相乗り」構成を復活させる。まさにこのアプローチが、冒頭で指摘した保守の大変さの問題を解決してくれます。共用クラスタが負荷に応じて自動的にスケールするので、チームはもう、自前の環境を抱えてそのリソースを見張ることを考えなくてよいのです。
インフラの構築には、自社のサービスやクラウドの製品を自由に使えます。そこでYandex Managed Service for Kubernetes®でクラスタを立ち上げ、ありとあらゆるリソースをTerraformで記述しました。あわせて、プロジェクトごとに独立したシークレット管理を整え、個々のリソースに対するアクセス権限をアトミックに設定しました。その成果が、再利用可能なTerraformモジュールです。これを使えば、Farmのk8sインストール環境をかなり手軽にデプロイできます。
わずか1日で、現行の全プロジェクトをFarmのk8sインストール環境へ無事に移し終えました。元々の契約と設定はそのままだったので、さほど難しくはありませんでした。新しいFarmはなかなかの好調ぶりを見せ、ほかのプロジェクトも続々と移行を始めました。ほどなくして、新しい問題が浮上します。クラスタはRAMとCPUについてはスケールするものの、ディスク領域は相変わらず埋まり続けたのです。毎日大量のイメージがビルドされ、ストレージを食っていたからです。
ここでは正面突破で、クリーナーの仕組みを追加して解決しました。
-
Dockerではこう動きます。未使用イメージを削除する時刻をcron式で指定します。
-
k8sでは、この仕組みはネイティブのCronJobリソースの上に組まれています。各ノードでPodが起動し、同じように古いイメージを削除します。
k8s版Farmの実験は成功だったと私たちは判断しました。根拠はチームからの反応、つまりフィードバックの内容、問題の件数、メンテナーへの問い合わせの数です。
現在のFarmにできること
この道のりを経て、Farmは単一スタック向けの「働き者」から、プレビュー環境の成熟したオーケストレーターへと成長しました。いまできることを手短にまとめます。
-
ブランチからのビルドと起動。 WebhookまたはUIをトリガーに、Farmがコードを取得し、アプリケーションをビルドして、専用URLを持つ隔離されたインスタンスを立ち上げます。
-
複数のプロバイダー。 水平スケーリングを伴う大規模なインストール環境にはk8s、クラスタを持たない1台の仮想マシンにはdocker。アプリケーションの契約はどのプロバイダーでも共通です。
-
プラグインレジストリを備えた設定可能なコア。 プロバイダー、バージョン管理システム、認証、Webhookアクションが単一のレジストリを通じて接続され、コアを変更せずにFarmを拡張できます。
-
スケジューラーとビルドキュー。 生成リクエストはキューに入り、スケジューラーが同時ビルド数と起動中インスタンス数の上限を守りながらキューを処理していきます。
-
ライフサイクル管理。 UIからのインスタンスの自動起動に加え、遊休インスタンスのタイムアウトによる停止と削除。
-
ヘルスチェック。 Farmはインスタンスの状態を監視し、最新のステータスをUIとAPIに反映します。Dockerとプロセスモードでは独自の死活チェックで、Kubernetesではネイティブのliveness/readinessプローブで行います。
-
環境変数の引き渡し。 柔軟な環境設定が可能です。ビルド時変数と実行時変数、生成時に上書きできない保護付き変数、さらにDockerではホストやコンテナからの変数の継承も使えます。
-
マイグレーション対応の永続データベース。 SQLite、PostgreSQL、あるいはその他のDBMSの上で動くKnex。
-
UIとAPI。 インスタンスの起動とデバッグのためのWebインターフェースと、CI連携のためのHTTP API。
-
ディスク領域のクリーナー。 Dockerとk8sにおける未使用イメージの定期的な掃除。
今後の展望
さて、今後の計画です。Farmにはまだまだ改善したいことがたくさんあります。現時点での主な方向性は次のとおりです。
-
k8sプロバイダーにおける、Ingressの置き換えとしてのGateway APIへの移行。
-
エイリアスのサポート。ハッシュではなく、人間が読める名前でインスタンスへアクセスできるようにします。
-
本格的な認可の仕組み。ユーザー、ロール、アクセス権限のシステムです。
-
プロジェクトごとの個別クォータ。
ほかのオープンソースプロジェクトと同じように、これからもドキュメントとサンプルを磨き続けます。Farmをもっと簡単・快適に使えるようにし、外部ユーザーの数が勢いよく増えていくようにするためです。ゆくゆくはFarmが、業界でプレビュー環境の定番ツールの1つになってくれたらと思っています。とはいえ、そこは成り行きを見守りましょう。
試してみるには
ぜひ私たちのリポジトリを訪ねて、READMEと関連ドキュメントをご覧ください。試してみるだけなら、Docker Engineがインストールされた環境で、Farmをローカルに気軽に立ち上げられます。どんなフィードバックも大歓迎です。Issueを立て、PRを送り、質問を投げかけ、追加の機能が必要なら機能リクエストもお寄せください!
最後までお読みいただきありがとうございました!プロジェクトを気に入っていただけたら、スターをいただけると嬉しいです!Yandex Cloudチームの最新ニュースに興味のある方は、私たちのチャンネルにご参加ください。

Mikhail Golbakh
Yandex Cloudのシニアフロントエンド開発者