Cloud Foundry Advent Calendar 2017の24日目です。
Open Service Broker APIについて。
目次
- Open Service Broker APIとは
- 簡単なOpen Service Brokerを実装する
- Open Service Brokerの登録
- Kubernetes上でPostgreSQLを動的にデプロイするOpen Service Brokerを実装する
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(username
、password
など)作成を依頼し、作成された情報(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を使って、
ServiceInstance
、ServiceBinding
と言ったCustomResourceを作成できるようになっています。
これにより、例えば
- Cloud Foundry向けに作成したOpen Service BrokerをKubernetesから使って
ServiceInstance
、ServiceBinding
リソースを作成する - 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の情報を取得するためのAPIPUT /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のRouter 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情報を参照可能になります。
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
status
にSuccessfully 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: ""
status
にThe 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: ""
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は次のコマンドで確認できます。
$ 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を使えることがわかりました。 今のエコシステムではMySQLやRabbitMQ、Redisといった データサービス用のService BrokerがBOSH上で利用可能です。 永続データを扱わないService BrokerであればCloud Foundryでデプロイできるものもあります。 これらの既存のService BrokerをKubernetesから利用可能です。
Cloud Foundryユーザーとしては、逆のパターン、すなわちKubernetes上でKubernetes上にクラスタをデプロイするService Brokerが使えるとが良いですよね。
Cloud Foundryではデータベースなどのステートフルアプリケーションは扱えないので、BOSHの変わりにKubernete上で手軽にデータベースをプロビジョニングして、 アプリケーションにバインドできると嬉しいです。
ということで、PostgresSQLをKubernetes上でプロビジョニングするOpen Service Brokerを作成し、Cloud Foundryから使ってみました。
解説は大変なので、とりあえずデモ動画を。
Demo: creating and using a PostgreSQL instance on Kubernetes from Cloud Foundry by cf create-service/bind-service via Open Service Broker API pic.twitter.com/BKBji6irNJ
— Toshiaki Maki (@making) December 25, 2017
ソースコードはこちらです。
Service Brokerからfabric8io/kubernetes-client
を使って動的にPostgreSQLをプロビジョニングしています。