--- title: Open Service Broker APIを使ってCloud FoundryとKubernetesでService Brokerを相互運用する tags: ["Cloud Foundry", "Kubernetes", "Open Service Broker API", "Spring WebFlux"] categories: ["Dev", "PaaS", "CloudFoundry"] date: 2017-12-24T09:59:40Z updated: 2017-12-31T03:54:37Z --- [Cloud Foundry Advent Calendar 2017](https://qiita.com/advent-calendar/2017/cloudfoundry)の24日目です。 Open Service Broker APIについて。 **目次** ### Open Service Broker APIとは Cloud Foundryユーザーなら[Service Broker](https://docs.cloudfoundry.org/services/overview.html)に馴染みが深いと思います。 データベースやメッセージキュー、キーバリューストアなどのデータストアや外部サービスのInstanceのプロビジョニングやCredentialsの作成を Platform(Cloud Foundry)にお任せするための仕組みです。 次の図のように、`cf create-service`コマンドを実行することで、Cloud ControllerがService Brokerに対して、新規Service Instanceのプロビジョンを依頼します。 作成されたInstanceに対して`cf bind-service`コマンドを実行することで、Credentials(`username`、`password`など)作成を依頼し、作成された情報(Service Binding)をアプリケーションの環境変数に埋め込みます(bindという)。 アプケーションへのbindが不要で、Credentialsだけ作成したい場合は`cf create-service-key`コマンドが使えます。 ![](https://docs.cloudfoundry.org/services/images/managed-services.png) このService Brokerの仕組みはとても便利で、Cloud Foundryだけに閉じるのはもったいないと言うことで他のPlatform(要はKubernetes)でも使えるように仕様を整理するために、 [Open Service Broker API](https://www.openservicebrokerapi.org/)という仕様ができました。 Cloud FoundryのCloud Controllerはこちらを参照するようになっています。 また、Kubernetesでも[Service Catalog](https://kubernetes.io/docs/concepts/service-catalog/)がOpen Service Broker APIを使って、 `ServiceInstance`、`ServiceBinding`と言った[CustomResource](https://kubernetes.io/docs/concepts/api-extension/custom-resources/)を作成できるようになっています。 これにより、例えば * Cloud Foundry向けに作成したOpen Service BrokerをKubernetesから使って`ServiceInstance`、`ServiceBinding`リソースを作成する * Kubernetes向けに作成したOpen Service BrokerをCloud Foundryから使ってService Instance、Service Bindingを作成する と言ったことが可能になります。 [Pivotal Cloud Foundry 2.0](https://content.pivotal.io/blog/achieving-escape-velocity-with-pivotal-cloud-foundry-2-0)の記事を見ると、 次のようにPAS(Pivotal Application Service = いわゆるCloud Foundry)とPKS(Pivotal Container Service = BOSHでKubernetesクラスタを動的にプロビジョニングできる新サービス)間で Open Service Brokerを使って、サービスを共有する狙いが見えます。(さらにはNSX-Tを使ってネットワークの共有まで??) ![](https://content.cdntwrk.com/files/aHViPTYzOTc1JmNtZD1pdGVtZWRpdG9yaW1hZ2UmZmlsZW5hbWU9aXRlbWVkaXRvcmltYWdlXzVhMzlhYmMxODAzZTYucG5nJnZlcnNpb249MDAwMCZzaWc9YTg5NjJiNGMzMzE4YTY3MTE4ODRkMmZmNjQ1MTRkYTQ%253D) 夢が広がりますね。 ### 簡単なOpen Service Brokerを実装する (OK、概念はわかった。実装してみよう) Open Service Brokerの作成は簡単です。仕様は[GitHub](https://github.com/openservicebrokerapi/servicebroker/blob/master/spec.md)で参照できますが、 次の7のHTTPエンドポイントを作成すれば良いです。 * `GET /v2/catalog` ... Service Brokerの情報を取得するためのAPI * `PUT /v2/service_instances/:instance_id` ... Service InstanceをプロビジョニングするためのAPI (`cf create-service`に相当) * `GET /v2/service_instances/:instance_id/last_operation` ... Service Instanceの作成状態を確認するためのAPI (非同期でプロビジョニングした際に使用する) * `PATCH /v2/service_instances/:instance_id` ... Service InstanceをアップデートするためのAPI (`cf update-service`に相当) * `DELETE /v2/service_instances/:instance_id` ... Service Instanceを削除するためのAPI (`cf delete-service`に相当) * `PUT /v2/service_instances/:instance_id/service_bindings/:binding_id` ... Service Bindingを作成するためのAPI (`cf bind-service`または`cf create-service-key`に相当) * `DELETE /v2/service_instances/:instance_id/service_bindings/:binding_id` ... Service InstanceをアップデートするためのAPI (`cf unbind-service`または`cf delete-service-key`に相当) #### Spring WebFluxで実装 この仕様を[Spring WebFlux](https://docs.spring.io/spring-framework/docs/5.0.x/spring-framework-reference/web-reactive.html)の[Router Functions](https://docs.spring.io/spring-framework/docs/5.0.x/spring-framework-reference/web-reactive.html#webflux-fn)で実装してみます。 何もしないService Brokerは次のコードで実装できます。このコードはテンプレートとして利用可能です。 Spring Bootを使用しない、軽量アプリとして実装しています。(この作り方自体に興味がある方は[こちら](https://github.com/making/vanilla-spring-webflux-fn-blank)を) `ServiceBrokerHander.java` ```java package com.example.demoosbapi; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.util.CollectionUtils; import org.springframework.web.reactive.function.server.HandlerFunction; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.server.ResponseStatusException; import org.yaml.snakeyaml.Yaml; import reactor.core.publisher.Mono; import java.io.IOException; import java.io.InputStream; import java.util.Base64; import java.util.List; import java.util.UUID; import static java.util.Collections.emptyMap; import static org.springframework.web.reactive.function.server.RequestPredicates.*; import static org.springframework.web.reactive.function.server.RouterFunctions.route; import static org.springframework.web.reactive.function.server.ServerResponse.ok; import static org.springframework.web.reactive.function.server.ServerResponse.status; public class ServiceBrokerHandler { private static final Logger log = LoggerFactory.getLogger(ServiceBrokerHandler.class); private final Object catalog; private final ObjectMapper objectMapper = new ObjectMapper(); private final Mono badRequest = Mono.defer(() -> Mono .error(new ResponseStatusException(HttpStatus.BAD_REQUEST))); public ServiceBrokerHandler(Resource catalog) throws IOException { Yaml yaml = new Yaml(); try (InputStream stream = catalog.getInputStream()) { this.catalog = yaml.load(stream); } } public RouterFunction routes() { final String serviceInstance = "/v2/service_instances/{instanceId}"; final String serviceBindings = "/v2/service_instances/{instanceId}/service_bindings/{bindingId}"; return route(GET("/v2/catalog"), this::catalog) .andRoute(PUT(serviceInstance), this::provisioning) .andRoute(PATCH(serviceInstance), this::update) .andRoute(GET(serviceInstance + "/last_operation"), this::lastOperation) .andRoute(DELETE(serviceInstance), this::deprovisioning) .andRoute(PUT(serviceBindings), this::bind) .andRoute(DELETE(serviceBindings), this::unbind) .filter(this::versionCheck) .filter(this::basicAuthentication); } Mono catalog(ServerRequest request) { return ok().syncBody(catalog); } Mono provisioning(ServerRequest request) { String instanceId = request.pathVariable("instanceId"); log.info("Provisioning instanceId={}", instanceId); return request.bodyToMono(JsonNode.class) // .filter(this::validateMandatoryInBody) // .filter(this::validateGuidInBody) // .flatMap(r -> { ObjectNode res = this.objectMapper.createObjectNode() // .put("dashboard_url", "http://example.com"); return status(HttpStatus.CREATED).syncBody(res); }) // .switchIfEmpty(this.badRequest); } Mono update(ServerRequest request) { String instanceId = request.pathVariable("instanceId"); log.info("Updating instanceId={}", instanceId); return request.bodyToMono(JsonNode.class) // .filter(this::validateMandatoryInBody) // .flatMap(r -> ok().syncBody(emptyMap())) // .switchIfEmpty(this.badRequest); } Mono deprovisioning(ServerRequest request) { String instanceId = request.pathVariable("instanceId"); log.info("Deprovisioning instanceId={}", instanceId); if (!this.validateParameters(request)) { return this.badRequest; } return ok().syncBody(emptyMap()); } Mono lastOperation(ServerRequest request) { return ok().syncBody(this.objectMapper.createObjectNode() // .put("state", "succeeded")); } Mono bind(ServerRequest request) { String instanceId = request.pathVariable("instanceId"); String bindingId = request.pathVariable("bindingId"); log.info("bind instanceId={}, bindingId={}", instanceId, bindingId); return request.bodyToMono(JsonNode.class) // .filter(this::validateMandatoryInBody) // .flatMap(r -> { ObjectNode res = this.objectMapper.createObjectNode(); res.putObject("credentials") // .put("username", UUID.randomUUID().toString()) // .put("password", UUID.randomUUID().toString()); return status(HttpStatus.CREATED).syncBody(res); }) // .switchIfEmpty(this.badRequest); } Mono unbind(ServerRequest request) { String instanceId = request.pathVariable("instanceId"); String bindingId = request.pathVariable("bindingId"); log.info("unbind instanceId={}, bindingId={}", instanceId, bindingId); if (!this.validateParameters(request)) { return this.badRequest; } return ok().syncBody(emptyMap()); } private boolean validateParameters(ServerRequest request) { return request.queryParam("plan_id").isPresent() && request.queryParam("service_id").isPresent(); } private boolean validateMandatoryInBody(JsonNode node) { return node.has("plan_id") && node.get("plan_id").asText().length() == 36 // TODO && node.has("service_id") && node.get("service_id").asText().length() == 36; // TODO } private boolean validateGuidInBody(JsonNode node) { return node.has("organization_guid") && node.has("space_guid"); } private Mono basicAuthentication(ServerRequest request, HandlerFunction function) { if (request.path().startsWith("/v2/")) { List authorizations = request.headers().header(HttpHeaders.AUTHORIZATION); String basic = Base64.getEncoder().encodeToString("username:password".getBytes()); if (authorizations.isEmpty() || authorizations.get(0).length() <= "Basic ".length() || !authorizations.get(0).substring("Basic ".length()).equals(basic)) { return status(HttpStatus.UNAUTHORIZED) .syncBody(this.objectMapper.createObjectNode() // .put("description", "Unauthorized.")); } } return function.handle(request); } private Mono versionCheck(ServerRequest request, HandlerFunction function) { List apiVersion = request.headers().header("X-Broker-API-Version"); if (CollectionUtils.isEmpty(apiVersion)) { return status(HttpStatus.PRECONDITION_FAILED) .syncBody(this.objectMapper.createObjectNode() // .put("description", "X-Broker-API-Version header is missing.")); } return function.handle(request); } } ``` `DemoOsbapiApplication.java` ``` java package com.example.demoosbapi; import org.slf4j.LoggerFactory; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import reactor.ipc.netty.http.server.HttpServer; import java.io.IOException; import java.io.UncheckedIOException; import java.util.Optional; public class DemoOsbapiApplication { private static RouterFunction routes() { Resource catalog = new ClassPathResource("catalog.yml"); try { return new ServiceBrokerHandler(catalog).routes(); } catch (IOException e) { throw new UncheckedIOException(e); } } public static void main(String[] args) throws Exception { long begin = System.currentTimeMillis(); int port = Optional.ofNullable(System.getenv("PORT")) // .map(Integer::parseInt) // .orElse(8080); HttpServer httpServer = HttpServer.create("0.0.0.0", port); httpServer.startRouterAndAwait(routes -> { HttpHandler httpHandler = RouterFunctions.toHttpHandler(routes()); routes.route(x -> true, new ReactorHttpHandlerAdapter(httpHandler)); }, context -> { long elapsed = System.currentTimeMillis() - begin; LoggerFactory.getLogger(DemoOsbapiApplication.class).info("Started in {} seconds", elapsed / 1000.0); }); } } ``` CatalogはYAMLで定義するようにしています。 `catalog.yml` ``` yaml --- services: - id: b98aea8a-9961-44bc-b68e-627b5d495a94 # must be unique name: demo # must be unique description: Demo bindable: true planUpdateable: false requires: [] tags: - demo metadata: displayName: Demo documentationUrl: https://twitter.com/making imageUrl: https://avatars2.githubusercontent.com/u/19211531 longDescription: Demo providerDisplayName: "@making" supportUrl: https://twitter.com/making plans: - id: dcb86c66-274e-44c0-941d-d78cacd12ccc # must be unique name: demo description: Demo free: true metadata: displayName: Demo bullets: - Demo costs: - amount: usd: 0 unit: MONTHLY ``` CatalogのService Id、Service NameとPlan IdはService Brokerの登録先で一意になる必要があります。 全コードは[こちら](https://github.com/making/demo-osbapi)。 #### ビルドとデプロイ Spring Boot Maven Pluginで実行可能jarを作ります。 ``` mvn clean package -DskipTests=true ``` 今回のService Brokerはダミーレスポンスを返すだけのステートレスアプリケーションなので、 デプロイ先はどこでも良いです。 今回は[Pivotal Web Services](https://run.pivotal.io/)にデプロイします。 次の`manifest.yml`を用意します。今回はSprring Bootを使っていないので軽量なJavaアプリです。もう少し小さくできますが、`256m`メモリにします。 Java Buildpackの[Java Memory Calculator](https://github.com/cloudfoundry/java-buildpack-memory-calculator)の設定を変更して、 256MBでも動くようにします。 ``` yaml applications: - name: demo-osbapi path: target/demo-osbapi-0.0.1-SNAPSHOT.jar memory: 256m env: JAVA_OPTS: '-XX:ReservedCodeCacheSize=32M -XX:MaxDirectMemorySize=32M' JBP_CONFIG_OPEN_JDK_JRE: '[memory_calculator: {stack_threads: 30}]' ``` `cf push`でデプロイ可能です。 ``` cf push ``` jarを作れば`cf push`でアプリケーションを即デプロイできるのがCloud Foundryの非常に良いところです。 > 256MBしか使わないので[IBM Cloudライト・アカウント](https://www.ibm.com/cloud-computing/jp/ja/bluemix/lite-account/)の無料枠でも利用可能ですね(!!)。でもPivotal Web Servicesも使ってね。 動作確認のためにCatalog APIにアクセスしてみます。 ``` json $ curl -s -u username:password -H "X-Broker-API-Version: 2.13" https://demo-osbapi.cfapps.io/v2/catalog | jq . { "services": [ { "id": "b98aea8a-9961-44bc-b68e-627b5d495a94", "name": "demo", "description": "Demo", "bindable": true, "planUpdateable": false, "requires": [], "tags": [ "demo" ], "metadata": { "displayName": "Demo", "documentationUrl": "https://twitter.com/making", "imageUrl": "https://avatars2.githubusercontent.com/u/19211531", "longDescription": "Demo", "providerDisplayName": "@making", "supportUrl": "https://twitter.com/making" }, "plans": [ { "id": "dcb86c66-274e-44c0-941d-d78cacd12ccc", "name": "demo", "description": "Demo", "free": true, "metadata": { "displayName": "Demo", "bullets": [ "Demo" ], "costs": [ { "amount": { "usd": 0 }, "unit": "MONTHLY" } ] } } ] } ] } ``` OKです。 ### Open Service Brokerの登録 では作成したOpen Service BrokerをCloud Foundry、Kubernetes両方に登録して利用してみます。 #### Cloud Foundry上でOpen Service Brokerを利用する まずはCloud Foundryから。 ##### Open Service Brokerの登録 Cloud FoundryでService Brokerを登録して、全Organizationに公開するにはAdmin権限が必要です。 Admin権限がある場合は、次のコマンドでOpen Service Brokerを登録可能です。 ``` cf create-service-broker demo username password https://demo-osbapi.cfapps.io cf enable-service-access demo ``` PublicなCloud Foundryサービスなどを利用している場合は当然Admin権限はないので、その場合は`--space-scoped`オプションをつけて 自分のSpaceに限定して登録することができます。 ``` cf create-service-broker demo username password https://demo-osbapi.cfapps.io --space-scoped ``` > PublicなCloud Foundryで試す場合は、`catalog.yml`の修正して、uniqueにすべき値を変更してください。 Open Service Brokerが登録できれば`cf marketplace`コマンドでMarketplaceに登録されていることが確認できます。 ``` $ cf marketplace Getting services from marketplace in org ikam / space home as admin... OK service plans description demo demo Demo ``` Pivotal Web Serviceで登録した場合は、Apps ManagerのMarketplaceからCatalog情報を参照可能になります。 ![image](https://user-images.githubusercontent.com/106908/34324010-ca57525c-e8a3-11e7-831e-1c687d9d23da.png) ##### Service Instanceの作成 `cf create-service`コマンドでService Instance作成できます。 ``` cf create-service demo demo hello ``` `cf services`コマンドで作成したSerivce Instanceを一覧で表示できます。 ``` $ cf services Getting services in org ikam / space home as admin... OK name service plan bound apps last operation hello demo demo create succeeded ``` ##### Service Bindingの作成 `cf bind-service `でService Bindingを作成してその情報をアプリケーションの環境変数に埋め込めるのですが、 今回はアプリケーションがないので、`cf service-key`コマンドを使用してService Bindingを作成するに止めます。 > Service KeyはバックエンドサービスにCloud Foundry上のアプリケーション以外からアクセスするCredentialsを発行する際によく利用します。 > 例えば、データベースに対してCLIからアクセスしたい場合はService Keyを作成します。 ``` cf create-service-key hello hello-key ``` 作成したService Keyは`cf service-key`コマンドで確認できます。 ``` $ cf service-key hello hello-key Getting key hello-key for service instance hello as admin... { "password": "f78538ee-2c5f-48d5-b519-7da00066a657", "username": "f59be4ad-27ba-4b71-86df-5da7b66489f0" } ``` `cf bind-service`でアプリケーションにBindした場合は、アプリケーションの環境変数`VCAP_SERVICES`の中に次のようなJSONが設定されます。 ``` json { "demo": [ { "binding_name": null, "credentials": { "password": "613ac8f5-bc47-4f60-b446-f9d4a13736f8", "username": "58212a8f-61de-4d34-968a-99dcb4770688" }, "instance_name": "hello", "label": "demo", "name": "hello", "plan": "demo", "provider": null, "syslog_drain_url": null, "tags": [ "demo" ], "volume_mounts": [] } ] } ``` #### Kubernetes上でOpen Service Brokerを利用する 次にKubernetesで。 ここではMinkubeを使います。次のコマンドでMinikubeを立ち上げました。 ``` minikube start --extra-config=apiserver.Authorization.Mode=RBAC --memory=4096 kubectl create clusterrolebinding tiller-cluster-admin \ --clusterrole=cluster-admin \ --serviceaccount=kube-system:default ``` ##### Service Catalogのインストール Service Catalogの仕組みはKubernetesにはデフォルトでは含まれていないので、別途インストールする必要があります。 インストール方法は[こちら](https://kubernetes.io/docs/tasks/service-catalog/install-service-catalog-using-helm/)。 [Helm](https://docs.helm.sh)を使います。 ``` helm init helm repo add svc-cat https://svc-catalog-charts.storage.googleapis.com helm install svc-cat/catalog --name catalog --namespace catalog --set insecure=true ``` Service Catalogがインストールされていることを次のコマンドで確認できます。 ``` $ kubectl get all -n catalog NAME READY STATUS RESTARTS AGE po/catalog-catalog-apiserver-688df64f85-xk6h6 2/2 Running 0 3m po/catalog-catalog-controller-manager-7f4568c7f6-x96rh 1/1 Running 0 2m NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE svc/catalog-catalog-apiserver 10.103.238.29 443:30443/TCP 6m NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE deploy/catalog-catalog-apiserver 1 1 1 1 6m deploy/catalog-catalog-controller-manager 1 1 1 1 6m NAME DESIRED CURRENT READY AGE rs/catalog-catalog-apiserver-688df64f85 1 1 1 6m rs/catalog-catalog-controller-manager-7f4568c7f6 1 1 1 6m ``` ##### Open Service Brokerの登録 Open Service Broker関連のリソースを配置するNamespaceを作成します。 ``` kubectl create namespace osb ``` Service Brokerを登録する`service-broker.yml`を作成します。 ``` yaml apiVersion: v1 kind: Secret metadata: name: demo-broker-secret namespace: osb type: Opaque data: username: dXNlcm5hbWU= # echo -n username | base64 password: cGFzc3dvcmQ= # echo -n password | base64 --- apiVersion: servicecatalog.k8s.io/v1beta1 kind: ClusterServiceBroker metadata: name: demo spec: url: https://demo-osbapi.cfapps.io authInfo: basic: secretRef: name: demo-broker-secret namespace: osb ``` 登録します。 ``` kubectl apply -f service-broker.yml -n osb ``` 状態を確認します。 ``` yaml $ kubectl get clusterservicebrokers/demo -o yaml apiVersion: servicecatalog.k8s.io/v1beta1 kind: ClusterServiceBroker metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"servicecatalog.k8s.io/v1beta1","kind":"ClusterServiceBroker","metadata":{"annotations":{},"name":"demo","namespace":""},"spec":{"authInfo":{"basic":{"secretRef":{"name":"demo-broker-secret","namespace":"osb"}}},"url":"https://demo-osbapi.cfapps.io"}} creationTimestamp: 2017-12-24T09:24:29Z finalizers: - kubernetes-incubator/service-catalog generation: 1 name: demo resourceVersion: "5" selfLink: /apis/servicecatalog.k8s.io/v1beta1/clusterservicebrokers/demo uid: 3e4d85c8-e88c-11e7-8c25-0242ac110005 spec: authInfo: basic: secretRef: name: demo-broker-secret namespace: osb relistBehavior: Duration relistDuration: 15m0s relistRequests: 0 url: https://demo-osbapi.cfapps.io status: conditions: - lastTransitionTime: 2017-12-24T09:24:36Z message: Successfully fetched catalog entries from broker. reason: FetchedCatalog status: "True" type: Ready lastCatalogRetrievalTime: 2017-12-24T09:24:36Z reconciledGeneration: 1 ``` `status`に`Successfully fetched catalog entries from broker.`と出ていればOKです。 (Service Brokerにはnamespaceはない模様) Catalog中のServiceやPlanの情報は次のコマンドで取得できます。 ``` yaml $ kubectl get clusterserviceclasses -o yaml -n osb apiVersion: v1 items: - apiVersion: servicecatalog.k8s.io/v1beta1 kind: ClusterServiceClass metadata: creationTimestamp: 2017-12-24T09:24:36Z name: b98aea8a-9961-44bc-b68e-627b5d495a94 namespace: "" resourceVersion: "3" selfLink: /apis/servicecatalog.k8s.io/v1beta1/clusterserviceclasses/b98aea8a-9961-44bc-b68e-627b5d495a94 uid: 42980446-e88c-11e7-8c25-0242ac110005 spec: bindable: true binding_retrievable: false clusterServiceBrokerName: demo description: Demo externalID: b98aea8a-9961-44bc-b68e-627b5d495a94 externalMetadata: displayName: Demo documentationUrl: https://twitter.com/making imageUrl: https://avatars2.githubusercontent.com/u/19211531 longDescription: Demo providerDisplayName: '@making' supportUrl: https://twitter.com/making externalName: demo planUpdatable: false tags: - demo status: removedFromBrokerCatalog: false kind: List metadata: resourceVersion: "" selfLink: "" $ kubectl get clusterserviceplans -o yaml -n osb apiVersion: v1 items: - apiVersion: servicecatalog.k8s.io/v1beta1 kind: ClusterServicePlan metadata: creationTimestamp: 2017-12-24T09:24:36Z name: dcb86c66-274e-44c0-941d-d78cacd12ccc namespace: "" resourceVersion: "4" selfLink: /apis/servicecatalog.k8s.io/v1beta1/clusterserviceplans/dcb86c66-274e-44c0-941d-d78cacd12ccc uid: 42a6872d-e88c-11e7-8c25-0242ac110005 spec: clusterServiceBrokerName: demo clusterServiceClassRef: name: b98aea8a-9961-44bc-b68e-627b5d495a94 description: Demo externalID: dcb86c66-274e-44c0-941d-d78cacd12ccc externalMetadata: bullets: - Demo costs: - amount: usd: 0 unit: MONTHLY displayName: Demo externalName: demo free: true status: removedFromBrokerCatalog: false kind: List metadata: resourceVersion: "" selfLink: "" ``` ##### Service Instanceの作成 次にこのService Brokerを使って、Kubernete用にService Instanceを作成しましょう。 `cf create-service demo demo hello`相当の情報を`service-instance.yml`に定義します。 ``` yaml apiVersion: servicecatalog.k8s.io/v1beta1 kind: ServiceInstance metadata: name: hello spec: clusterServiceClassExternalName: demo clusterServicePlanExternalName: demo ``` これを適用します。 ``` kubectl apply -f service-instance.yml ``` Service Instance一覧は次のコマンドで確認できます。 ``` $ kubectl get serviceinstances NAME AGE hello 34s ``` 詳細は`-o yaml`をつけて確認できます。 ``` yaml $ kubectl get serviceinstances -o yaml apiVersion: v1 items: - apiVersion: servicecatalog.k8s.io/v1beta1 kind: ServiceInstance metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"servicecatalog.k8s.io/v1beta1","kind":"ServiceInstance","metadata":{"annotations":{},"name":"hello","namespace":"default"},"spec":{"clusterServiceClassExternalName":"demo","clusterServicePlanExternalName":"demo"}} creationTimestamp: 2017-12-24T09:26:16Z finalizers: - kubernetes-incubator/service-catalog generation: 1 name: hello namespace: default resourceVersion: "10" selfLink: /apis/servicecatalog.k8s.io/v1beta1/namespaces/default/serviceinstances/hello uid: 7db79a17-e88c-11e7-8c25-0242ac110005 spec: clusterServiceClassExternalName: demo clusterServiceClassRef: name: b98aea8a-9961-44bc-b68e-627b5d495a94 clusterServicePlanExternalName: demo clusterServicePlanRef: name: dcb86c66-274e-44c0-941d-d78cacd12ccc externalID: 6b5f6803-63a5-491d-9601-09c35367e116 updateRequests: 0 status: asyncOpInProgress: false conditions: - lastTransitionTime: 2017-12-24T09:26:22Z message: The instance was provisioned successfully reason: ProvisionedSuccessfully status: "True" type: Ready dashboardURL: http://example.com deprovisionStatus: Required externalProperties: clusterServicePlanExternalID: dcb86c66-274e-44c0-941d-d78cacd12ccc clusterServicePlanExternalName: demo orphanMitigationInProgress: false reconciledGeneration: 1 kind: List metadata: resourceVersion: "" selfLink: "" ``` `status`に`The instance was provisioned successfully`というメッセージが出ていればOKです。 ##### Service Bindingの作成 `cf create-service-key hello hello-key`相当の情報を`service-instance.yml`に定義します。 ``` yaml apiVersion: servicecatalog.k8s.io/v1beta1 kind: ServiceBinding metadata: name: hello-key spec: instanceRef: name: hello secretName: hello-key-secret ``` 適用します。 ``` kubectl apply -f service-binding.yml ``` Service Binding一覧は次のコマンドで確認できます。 ``` $ kubectl get servicebindings NAME AGE hello-key 18s ``` 詳細は`-o yaml`をつけて確認できます。 ``` yaml $ kubectl get servicebindings -o yaml apiVersion: v1 items: - apiVersion: servicecatalog.k8s.io/v1beta1 kind: ServiceBinding metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"servicecatalog.k8s.io/v1beta1","kind":"ServiceBinding","metadata":{"annotations":{},"name":"hello-key","namespace":"default"},"spec":{"instanceRef":{"name":"hello"},"secretName":"hello-key-secret"}} creationTimestamp: 2017-12-24T09:27:57Z finalizers: - kubernetes-incubator/service-catalog generation: 1 name: hello-key namespace: default resourceVersion: "13" selfLink: /apis/servicecatalog.k8s.io/v1beta1/namespaces/default/servicebindings/hello-key uid: ba7470b4-e88c-11e7-8c25-0242ac110005 spec: externalID: f71094e1-bb91-439d-a51e-c37653b7d553 instanceRef: name: hello secretName: hello-key-secret status: asyncOpInProgress: false conditions: - lastTransitionTime: 2017-12-24T09:28:04Z message: Injected bind result reason: InjectedBindResult status: "True" type: Ready externalProperties: {} orphanMitigationInProgress: false reconciledGeneration: 1 unbindStatus: Required kind: List metadata: resourceVersion: "" selfLink: "" ``` `status`に`Injected bind result`というメッセージが出ていればOKです。 Kubernetesの場合は、Credentialが`Secret`リソースとして登録されます。 ``` $ kubectl get secrets NAME TYPE DATA AGE default-token-4dvtz kubernetes.io/service-account-token 3 14m hello-key-secret Opaque 2 2m ``` 作成されたCredentialsは次のコマンドで確認できます。 ``` yaml $ kubectl get secret hello-key-secret -o yaml apiVersion: v1 data: password: Mjg5MjI0ZjQtMWFkNC00ZjVmLTllNWUtMjVmN2UyMDllY2Vm username: ZTJkMjFmMjEtMDViNC00NTY3LTgyNDYtNWM1YTA3M2VkZjFk kind: Secret metadata: creationTimestamp: 2017-12-24T09:28:04Z name: hello-key-secret namespace: default ownerReferences: - apiVersion: servicecatalog.k8s.io/v1beta1 blockOwnerDeletion: true controller: true kind: ServiceBinding name: hello-key uid: ba7470b4-e88c-11e7-8c25-0242ac110005 resourceVersion: "1174" selfLink: /api/v1/namespaces/default/secrets/hello-key-secret uid: be183ad6-e88c-11e7-88b5-080027f294c4 type: Opaque ``` Service Brokerによって自動的にBackend ServiceのCredentialとして`Secret`が生成されたことになります。 あとはこの`Secret`をPodに設定すれば良いです。 ``` yaml env: - name: SECURITY_USER_NAME valueFrom: secretKeyRef: name: hello-key-secret key: username - name: SECURITY_USER_PASSWORD valueFrom: secretKeyRef: name: hello-key-secret key: password ``` ### Kubernetes上でPostgreSQLを動的にデプロイするOpen Service Brokerを実装する さてCloud FoundryでもKuberneteでも同じようにService Brokerを使えることがわかりました。 今のエコシステムでは[MySQL](https://github.com/cloudfoundry/cf-mysql-release)や[RabbitMQ](https://github.com/pivotal-cf/cf-rabbitmq-multitenant-broker-release)、[Redis](https://github.com/pivotal-cf/cf-redis-release)といった データサービス用のService BrokerがBOSH上で利用可能です。 永続データを扱わないService BrokerであればCloud Foundryでデプロイできるものもあります。 これらの既存のService BrokerをKubernetesから利用可能です。 ![image](https://user-images.githubusercontent.com/106908/34325640-c7bb9a26-e8da-11e7-8d72-0d5d00796772.png) Cloud Foundryユーザーとしては、逆のパターン、すなわちKubernetes上でKubernetes上にクラスタをデプロイするService Brokerが使えるとが良いですよね。 ![image](https://user-images.githubusercontent.com/106908/34325641-cb979780-e8da-11e7-98bb-cb1549f9e840.png) Cloud Foundryではデータベースなどのステートフルアプリケーションは扱えないので、BOSHの変わりにKubernete上で手軽にデータベースをプロビジョニングして、 アプリケーションにバインドできると嬉しいです。 ということで、PostgresSQLをKubernetes上でプロビジョニングするOpen Service Brokerを作成し、Cloud Foundryから使ってみました。 解説は大変なので、とりあえずデモ動画を。 ソースコードは[こちら](https://github.com/making/k8sosb)です。 Service Brokerから[`fabric8io/kubernetes-client`](https://github.com/fabric8io/kubernetes-client)を使って動的にPostgreSQLをプロビジョニングしています。