cloudfoundry
OpenServiceBroker

[CloudFoundry] Serviceの概要、未来への展望

この記事は、Cloud Foundry Advent Calendar 2017 17日目の記事です。
(記事の一部で、ソースコード例としてPHPを利用しています)

概要

CloudFoundry(以下CF)としてユーザに提供されている機構で、(個人的にですが)初学者に対して最も説明しづらいものが、Service/Marketplaceです。非常に抽象的な概念ではありますが、それ故に奥深く、夢が広がるシステムなので、ぜひ理解を深めておきたい重要なポイントでもあります。

というわけで、今日は「CloudFoundryにおけるService」というテーマで、「そもそもServiceって何よ?」って方を中心に、色々な活用法、そして昨今のServiceに纏わる動向と展望を、自説も交えながら書いてみようと思います。よろしくお願いします:bow:

Serviceってなに??

「Serviceってなんだろう?」と漠然としている人も多いと思いますので、実際によくある事例を追いながらそのメリットを体感してみましょう。

開発あるある事例 その1 (クレデンシャル管理編)

Serviceの効果が端的に現れるユースケースとして、クレデンシャル管理が挙げられます。

CFでWebアプリをつくるとき、データを保管したい、メールや通知を送りたい、などの要件が出てきて、MariaDB/S3などのDBやストレージを用意したり、メールアカウントを作りに行く、というケースはよく発生します。そのとき、作成したDBインスタンスのhost名やpasswordなどのクレデンシャル情報を、どうやってアプリに渡しますか?

案1: ソースコードに直接commitする

Amazon RDSなどのDB管理コンソールで、、DBインスタンスを払い出した後、そのクレデンシャル情報を下記のように埋め込みました。

$username = 'production-dbuser'
$password = 'pa5sw0rD';
$host = 'mydb.com';
$db = 'mydbschema';
$connection = new PDO("mysql:dbname=$db;host=$host", $username, $password);

これは、ソースコードとしては環境依存度が高く、またリスキーな状態です。ACLがない状態でソースコードがpublicになろうものなら、たちまち攻撃を受けてしまうでしょう。
それに、開発環境・本番環境と複数の環境・DBを用意することになると、それぞれで別のソースコードを必要としてしまうことになります。これではリリースの度にソースコードを書き換えることになってしまい、リリース時のヒューマンエラーで障害が起こってもおかしくありません。

このような問題を解決するため、The Twelve-Factor App (日本語訳)- III. 設定 では、以下のような指針が示されています。

アプリケーションは時に設定を定数としてコード内に格納する。これはTwelve-Factorに違反している。
Twelve-Factorは設定をコードから厳密に分離することを要求する。設定はデプロイごとに大きく異なるが、コードはそうではない。

Twelve-Factor Appは設定を環境変数に格納する。 環境変数は、コードを変更することなくデプロイごとに簡単に変更できる。
設定ファイルとは異なり、誤ってリポジトリにチェックインされる可能性はほとんどない。

つまり、アプリの環境変数として設定してあげると良さそうですね。

案2: アプリの環境変数として記述する

Amazon RDSなどのDB管理コンソールで、DBインスタンスを払い出した後、そのクレデンシャル情報を環境変数として設定しました。

$username = $_ENV['DB_USERNAME'];
$password = $_ENV['DB_PASSWORD'];
$host = $_ENV['DB_HOST'];
$db = $_ENV['DB_DATABASE'];
$connection = new PDO("mysql:dbname=$db;host=$host", $username, $password);
manifest-prod.yml
---
applications:
- name: myapp
  env:
    DB_USERNAME: production-dbuser
    DB_PASSWORD: pa5sw0rD
    DB_HOST: mydb.com
    DB_DATABASE: mydbschema

(もしくは)

set-env-prod.sh
$ cf push myapp --no-start
$ cf set-env myapp DB_USERNAME production-dbuser
$ cf set-env myapp DB_PASSWORD pa5sw0rD
$ cf set-env myapp DB_HOST mydb.com
$ cf set-env myapp DB_DATABASE mydbschema
$ cf start myapp

12Factorに従って、クレデンシャル情報を環境変数から呼び出すようにしました!これならソースコード部分は公開しても問題なさそうですし、複数の環境にも対応できるようになりました!

...しかし、落ち着いて考えてみましょう。この環境が更に増えた時、皆さんはどうやって環境変数を管理することになるでしょうか?手っ取り早いのは、上記のmanifestファイル(ShellScriptファイル)を環境ごとに管理することだと思いますが、以下のような新たな課題が見えてきます。

  • そのmanifest(ShellScript)ファイルはどこに保管しますか?なくさないような安全な保管場所がありますか?
  • 毎回手動で実行しないとデプロイできない仕組みになってしまいませんか?CDツールによっては対応できない手順になってしまいませんか?

個人的なアプリであれば許容できるかもしれませんが、組織として対応するとなると...なかなか大変そうですね:sweat: しかしここでも、The Twelve-Factor App (日本語訳) - IV. バックエンドサービス を見ると、有効な指針が示されています。

それぞれのバックエンドサービスはリソースである。例えば、1つのMySQLデータベースはリソースである。2つのMySQLデータベース(アプリケーション層でのシャーディングに使う)は2つの異なるリソースと見なせる。Twelve-Factor Appはこれらのデータベースをアタッチされたリソースとして扱う。

アタッチする、という新しい概念が出てきました。つまり、アプリに対して、もっと疎結合に、気軽にくっつけたり、離したりできる新しい概念を与えてしまえばいいのです。しかし一体、どうやって作るのでしょうか? CFでは、これがまさに Service として定義されているものの正体です。早速実装例を見てみましょう。

案3: Serviceインスタンスとしてアプリにbindする

Marketplaceと呼ばれる場所に、現在ユーザが利用可能なServiceの一覧が示されています。cf CLIではcf marketplaceで確認できます。(下記はPivotal Web ServiceにおけるClearDB Serviceの例となります。)

$ cf marketplace
...
サービス                      プラン                                説明
cleardb                      spark, boost*, amp*, shock*          Highly available MySQL for your Apps.

Marketplaceに登録されているServiceであれば、cf create-serviceコマンドを使うことで、簡単にDBインスタンスを用意できます。

$ cf create-service cleardb spark mydb

これだけでDBインスタンスが作成されました。これをアプリ内で利用するには、アプリにbind、つまりTwelve Factorにおけるアタッチの操作が必要になります。これはmanifestファイルに記述するか、cf bind-serviceコマンドを利用して実行することができます。

manifest-prod.yml
---
applications:
- name: myapp
  services:
  - mydb

(もしくは)

$ cf bind-service myapp mydb

bindされたアプリには、ServiceインスタンスからVCAP_SERVICESという名前の環境変数を経由して、bindしたServiceインスタンスのクレデンシャル情報が自動的に与えられます

$ cf env myapp
...
システム提供:
{
 "VCAP_SERVICES": {
  "cleardb": [
   {
    "credentials": {
     "hostname": "hogehogehoge.com",
     "jdbcUrl": "jdbc:mysql://hogehogehoge.com/mydbschema?user=production-dbuser\u0026password=pa5sw0rD",
     "name": "mydb",
     "password": "pa5sw0rD",
     "port": "3306",
     "uri": "mysql://production-dbuser:pa5sw0rD@us-cdbr-iron-east-05.cleardb.net:3306/mydbschema?reconnect=true",
     "username": "production-dbuser"
    },
    "label": "cleardb",
    "name": "mydb",
    "plan": "spark",
...
   }
  ]
 }
} 
$vcap_services = json_decode(getenv('VCAP_SERVICES'),true);
$mydb = $vcap_services['cleardb'][0]['credentials'];
$username = $mydb['username'];
$password = $mydb['password'];
$host = $mydb['hostname'];
$db = $mydb['name'];

$connection = new PDO("mysql:dbname=$db;host=$host", $username, $password);

(上記はさらに、cf-helper-phpなどのライブラリを使うことで、更に直感的に書くこともできます。)

この案では、ユーザは最早クレデンシャル情報を管理することはなく、ただ環境変数VCAP_SERVICESから渡されたJSONの中身をパースすることだけ考えれば良くなります。環境を切り替えたい時は、下記のようにServiceインスタンスを新たに用意し、bind先を切り替えるだけで対応できるわけです。

$ cf unbind-service myapp mydb
$ cf bind-service myapp new-mydb
$ cf restage myapp

このように、ユーザのクレデンシャル管理におけるメリットのみならず、TwelveFactorにおけるバックエンドサービスとしての集大成がServiceである、とお分かり頂けたかと思います。

開発あるある事例 その2 (社内外クエスト編)

上記の事例でも発生したように、開発作業の中にはしばしば、用意を行うための事務的な手続き(俗に言うクエスト)が頻繁に発生します。

これらが発生した度にブラウザーを開いてWebフォームを埋めたり、作業手順書を見ていては開発速度も下がりますし、テンションもだだ下がりでしょう。もし、こういった作業がすべて、先ほどのMarketplaceに並んでいて、CLIでできた場合を想定してみましょう。(下記はPivotal Web Serviceにおける実行例となります。)

# MySQLインスタンスを用意する
$ cf create-service cleardb spark mydb

# SMTPアカウントを払い出す
$ cf create-service sendgrid free mymail

# Twitterでリプライ/DMを送るためのアカウントを設定する(取得は自前・設定にはUserProvidedServiceを利用)
$ cf create-user-provided-service mytwitter -p "{\"user\":\"piyo\",\"pass\":\"pa55woRD\"}"

# NewRelicのアカウントを新規取得する
$ cf create-service newrelic standard mynewrelic

# SSL証明書を設定する(取得は自前・設定はダッシュボードから)
$ cf create-service ssl basic ssl.myapp.com
manifest.yml
---
applications:
- name: myapp
  services:
  - mydb
  - mymail
  - mytwitter
  - mynewrelic
  - ssl.myapp.com

完全ではないものの、作業におけるかなりの量がCLIとmanifest.ymlで完結し、自動化されています。Webブラウザを開き続ける作業よりはテンションも下がりません(たぶん)。これらを社内運営するプラットフォーマーから見ても、このような日々の運用作業が完全に自動化できると考えれば垂涎モノです。このように、社内外の無駄な事務工数を減らしつつ、自動化を進めていく観点から見ても、Service/Marketplaceという技術は実に画期的な仕組みであると考えられます。

ServiceBroker APIをつくる(Marketplaceに自分たちのServiceをつくる・のせる)

「ウチのプラットフォームもCloudFoundryのMarketplaceにServiceとして載せて運用ラクにしたい!」と思った場合、ServiceBrokerと呼ばれる連携用のHTTP APIを作り、CloudFoundryの管理者に「ウチのServiceをMarketplaceに載せて!」というだけです。

詳しい説明は上記公式ドキュメントをご覧頂ければと思いますが、端的にまとめると下記7本のHTTP APIを作るだけでOKです(v2.13時点)。

  1. GET /v2/catalog
    • Marketplaceに掲載するServiceとしての基本情報(カタログ情報)を定義する
  2. PUT /v2/service_instances/:instance_id
    • cf create-serviceを行われた際にやるべきことを定義する
    •  (mysqlであれば、DBインスタンスの作成など)
  3. GET /v2/service_instances/:instance_id/last_operation
    • 2. を非同期で行う際のステータスのポーリング方法を定義する
  4. PATCH /v2/service_instances/:instance_id
    • cf update-serviceを行われた際にやるべきことを定義する
    •  (mysqlであれば、DBインスタンスの情報更新など)
  5. PUT /v2/service_instances/:instance_id/service_bindings/:binding_id
    • cf bind-serviceを行われた際にやるべきこと・アプリに渡すクレデンシャル情報を定義する
    •  (mysqlであれば、アクセス用ユーザの作成、DBインスタンスへの権限付与など)
  6. DELETE /v2/service_instances/:instance_id/service_bindings/:binding_id
    • cf unbind-serviceを行われた際にやるべきことを定義する
    • (mysqlであれば、DBインスタンスからの権限剥奪、アクセス用ユーザの削除など)
  7. DELETE /v2/service_instances/:instance_id
    • cf delete-serviceを行われた際にやるべきことを定義する
    • (mysqlであれば、DBインスタンスの削除など)

これらは既に、各言語別の実装例が多数OSSとして公開されています。上記実装例があるものであれば比較的取り組みやすいと思われますので、ぜひ一度ご覧ください。

Serviceの種類

Serviceにはいくつかのtypeがあり、それによって果たす役割が違うものがいくつか存在します。

Credentials

アプリに対してbindを行うことで、クレデンシャル情報を渡すタイプのServiceのことです。上記の例でも多数触れたように、もっとも汎用的に利用されるタイプのServiceとなります。

Log Drain

アプリに対してbindを行うことで、定義されたURLに対して、アプリケーションログを転送することを目的としたServiceのことです。Papertrailなどのサービスでの利用する場合に使われているケースがよく見られます。

Volume Services

アプリに対してbindを行うことで、CFアプリに対してファイルシステムをマウントできるようなアプリを開発できます。ストレージを要求するアプリ用途での利用が可能になります。

Route Services

やや特殊で、Routeに対してbindを行うことで、そのRoute宛のリクエストを定義されたURLでリバースプロキシするServiceを開発できます。主にファイアウォールやリクエスト数計測・制御などの用途で利用されることが多いですが、"1Routeに1Serviceまで"の制約があるため注意が必要です。また、リバースプロキシの携帯が複数存在するため、利用用途に応じて環境を検討することが可能です。

etc...

各社の事例を見ると、変わったアプローチのServiceも多数存在します。代表的なものを紹介します。

  • AutoScaler ... CFアプリのインスタンス数をCPU使用率、アプリのレイテンシなどを見て自動的に増減させる。credentialsだが、実際には何もアプリには渡さない。
  • Scheduler ... One-OffなTaskを一定時間おきに対象アプリ内のコンテナで自動実行する。CLIプラグインで独自開発したコマンドを活用し、bind情報から対象アプリ内でJobを実行している。

このように、実質的にはServiceじゃなくても良さそうなものまでもなんでもService化できてしまいますから、「APIの実装が許すならなんでもあり」と言えなくもないですね。

Serviceの可能性を考える

更に上記の要件を削ぎ落としてシンプルに考えれば、create-servicebind-serviceの仕様をとりあえず決められそうであるならば、ServiceBrokerで自動化できる要素はあるといえます。例えば、下記のようなケースであれば、API次第ではありますが、大いにService化できる・自動化できる可能性が出てくると言えるでしょう。

  • RDB/KVS/メッセージキュー/ストレージ/SMTPなどの、クレデンシャル情報を払い出す
  • ログを外部の見やすいプラットフォームに転送してメトリクス表示・通知を行う
  • SSL証明書をロードバランサに代理で設定する
  • DoS対策のPFを経由するようにRouteを設定する

どうですか? こんなことが全部CLIの、しかもcfコマンドだけでできるってなったら、ちょっとワクワクしませんか?

ServiceはCFでしか使えないの?

とは言え、クラウド時代のプラットフォームは、もちろんPaaSだけではありません。Kubenetesに代表されるCaaS(Container as a Service)、AWS Lambdaに代表されるFaaS(Function as a Service)など、多岐に渡ります。「CFでしか使えない仕組みに対応したところで、他のPFには対応できなくて無駄になるんじゃないの?」と懸念される方もいらっしゃるかもしれません。

しかし今年から動き始めたOpen Service Brokerというプロジェクトによって、ServiceはCFの枠組みを超えた取り組みに昇華されました。 これはCFだけでなく、OpenShift、Kubenetesなどの他のクラウドプラットフォーム上でも、Serviceの仕組みが使えるようにするための取り組みとなっています。つまり、今後はCFの自動化だけに留まらず、他のPF上での利活用も見据えた上で、検討を開始すべきものになっていくことでしょう。

Serviceは未来のプラットフォームの共通プロトコル

というと大袈裟ですが、TwelveFactorのシンプルな思想とそれを実現するServiceは、楽をしたいアプリ開発者・PF運用者にとって今後ますます注目すべき存在です。CFを触っていない人でも、cf pushしかやったことない人も、ぜひ一度、気軽にServiceの世界にDiveしてみてくださいね :ocean:

おしまい

今年はCFの動きが本当に活発で、仕事がとても楽しい1年でした。来年もどうぞよろしくお願いします :pray:

明日12/18は、 @Go-zen-chu さんです! お楽しみに!