📝 BLOG.IK.AM

@making's memo
(🗃 Categories 🏷 Tags)

Open Service Broker APIを使ってCloud FoundryとKubernetesでService Brokerを相互運用する

🗃 {Dev/PaaS/CloudFoundry}

🏷 Cloud Foundry 🏷 Kubernetes 🏷 Open Service Broker API 🏷 Spring WebFlux

🗓 Updated at 2017-12-31T12:54:37+09:00 by Toshiaki Maki  🗓 Created at 2017-12-24T18:59:40+09:00 by Toshiaki Maki  {✒️️ Edit  ⏰ History}


⚠️ Caution: This content is a bit old. Please be careful to read.

Cloud Foundry Advent Calendar 2017の24日目です。

Open Service Broker APIについて。

目次

Open Service Broker APIとは

Cloud FoundryユーザーならService Brokerに馴染みが深いと思います。

データベースやメッセージキュー、キーバリューストアなどのデータストアや外部サービスのInstanceのプロビジョニングやCredentialsの作成を
Platform(Cloud Foundry)にお任せするための仕組みです。

次の図のように、cf create-serviceコマンドを実行することで、Cloud ControllerがService Brokerに対して、新規Service Instanceのプロビジョンを依頼します。
作成されたInstanceに対してcf bind-serviceコマンドを実行することで、Credentials(usernamepasswordなど)作成を依頼し、作成された情報(Service Binding)をアプリケーションの環境変数に埋め込みます(bindという)。

アプケーションへのbindが不要で、Credentialsだけ作成したい場合はcf create-service-keyコマンドが使えます。

このService Brokerの仕組みはとても便利で、Cloud Foundryだけに閉じるのはもったいないと言うことで他のPlatform(要はKubernetes)でも使えるように仕様を整理するために、
Open Service Broker APIという仕様ができました。

Cloud FoundryのCloud Controllerはこちらを参照するようになっています。
また、KubernetesでもService CatalogがOpen Service Broker APIを使って、
ServiceInstanceServiceBindingと言ったCustomResourceを作成できるようになっています。

これにより、例えば

  • Cloud Foundry向けに作成したOpen Service BrokerをKubernetesから使ってServiceInstanceServiceBindingリソースを作成する
  • Kubernetes向けに作成したOpen Service BrokerをCloud Foundryから使ってService Instance、Service Bindingを作成する

と言ったことが可能になります。

Pivotal Cloud Foundry 2.0の記事を見ると、
次のようにPAS(Pivotal Application Service = いわゆるCloud Foundry)とPKS(Pivotal Container Service = BOSHでKubernetesクラスタを動的にプロビジョニングできる新サービス)間で
Open Service Brokerを使って、サービスを共有する狙いが見えます。(さらにはNSX-Tを使ってネットワークの共有まで??)

夢が広がりますね。

簡単なOpen Service Brokerを実装する

(OK、概念はわかった。実装してみよう)

Open Service Brokerの作成は簡単です。仕様はGitHubで参照できますが、
次の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 WebFluxRouter Functionsで実装してみます。
何もしないService Brokerは次のコードで実装できます。このコードはテンプレートとして利用可能です。

Spring Bootを使用しない、軽量アプリとして実装しています。(この作り方自体に興味がある方はこちらを)

ServiceBrokerHander.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<ServerResponse> 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<ServerResponse> 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<ServerResponse> catalog(ServerRequest request) {
        return ok().syncBody(catalog);
    }

    Mono<ServerResponse> 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<ServerResponse> 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<ServerResponse> 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<ServerResponse> lastOperation(ServerRequest request) {
        return ok().syncBody(this.objectMapper.createObjectNode() //
                .put("state", "succeeded"));
    }

    Mono<ServerResponse> 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<ServerResponse> 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<ServerResponse> basicAuthentication(ServerRequest request,
                                                     HandlerFunction<ServerResponse> function) {
        if (request.path().startsWith("/v2/")) {
            List<String> 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<ServerResponse> versionCheck(ServerRequest request,
                                              HandlerFunction<ServerResponse> function) {
        List<String> 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

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

---
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の登録先で一意になる必要があります。

全コードはこちら

ビルドとデプロイ

Spring Boot Maven Pluginで実行可能jarを作ります。

mvn clean package -DskipTests=true

今回のService Brokerはダミーレスポンスを返すだけのステートレスアプリケーションなので、
デプロイ先はどこでも良いです。

今回はPivotal Web Servicesにデプロイします。

次のmanifest.ymlを用意します。今回はSprring Bootを使っていないので軽量なJavaアプリです。もう少し小さくできますが、256mメモリにします。
Java BuildpackのJava Memory Calculatorの設定を変更して、
256MBでも動くようにします。

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ライト・アカウントの無料枠でも利用可能ですね(!!)。でもPivotal Web Servicesも使ってね。

動作確認のためにCatalog APIにアクセスしてみます。

$ 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

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 <app name> <service instance name>で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が設定されます。

{
  "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にはデフォルトでは含まれていないので、別途インストールする必要があります。
インストール方法はこちら

Helmを使います。

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   <nodes>       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を作成します。

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

状態を確認します。

$ 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

statusSuccessfully fetched catalog entries from broker.と出ていればOKです。

(Service Brokerにはnamespaceはない模様)

Catalog中のServiceやPlanの情報は次のコマンドで取得できます。

$ 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に定義します。

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をつけて確認できます。

$ 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: ""

statusThe instance was provisioned successfullyというメッセージが出ていればOKです。

Service Bindingの作成

cf create-service-key hello hello-key相当の情報をservice-instance.ymlに定義します。

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をつけて確認できます。

$ 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: ""

statusInjected 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は次のコマンドで確認できます。

$ 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に設定すれば良いです。

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を使えることがわかりました。
今のエコシステムではMySQLRabbitMQRedisといった
データサービス用のService BrokerがBOSH上で利用可能です。
永続データを扱わないService BrokerであればCloud Foundryでデプロイできるものもあります。
これらの既存のService BrokerをKubernetesから利用可能です。

image

Cloud Foundryユーザーとしては、逆のパターン、すなわちKubernetes上でKubernetes上にクラスタをデプロイするService Brokerが使えるとが良いですよね。

image

Cloud Foundryではデータベースなどのステートフルアプリケーションは扱えないので、BOSHの変わりにKubernete上で手軽にデータベースをプロビジョニングして、
アプリケーションにバインドできると嬉しいです。

ということで、PostgresSQLをKubernetes上でプロビジョニングするOpen Service Brokerを作成し、Cloud Foundryから使ってみました。

解説は大変なので、とりあえずデモ動画を。

ソースコードはこちらです。

Service Brokerからfabric8io/kubernetes-clientを使って動的にPostgreSQLをプロビジョニングしています。