Open Service Broker API Implementation: Building Service Marketplaces for Kubernetes Platforms
The Open Service Broker API (OSBAPI) provides a standard way for platforms to provision and manage backing services. By implementing service brokers, platform teams can offer self-service catalogs of databases, message queues, and other dependencies. This guide demonstrates building production-grade service brokers for enterprise Kubernetes platforms.
Open Service Broker API Implementation: Building Service Marketplaces for Kubernetes Platforms
Understanding Open Service Broker API
The Open Service Broker API defines a standard contract between platforms and service brokers, enabling:
- Service Catalog: Browseable catalog of available services
- Provisioning: On-demand service instance creation
- Binding: Generate credentials for applications
- Deprovisioning: Cleanup and resource deletion
- Updates: Modify existing service instances
API Endpoints
GET /v2/catalog - List available services
PUT /v2/service_instances/:id - Provision service instance
GET /v2/service_instances/:id - Get service instance
PATCH /v2/service_instances/:id - Update service instance
DELETE /v2/service_instances/:id - Deprovision service instance
PUT /v2/service_instances/:id/service_bindings/:binding_id - Create binding
GET /v2/service_instances/:id/service_bindings/:binding_id - Get binding
DELETE /v2/service_instances/:id/service_bindings/:binding_id - Delete binding
Project Setup
Go Service Broker Implementation
// main.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"github.com/gorilla/mux"
"github.com/pivotal-cf/brokerapi/v8"
"github.com/pivotal-cf/brokerapi/v8/domain"
"github.com/pivotal-cf/brokerapi/v8/middlewares"
)
type DatabaseBroker struct {
config BrokerConfig
store Store
}
type BrokerConfig struct {
Username string
Password string
Port string
}
func main() {
config := BrokerConfig{
Username: os.Getenv("BROKER_USERNAME"),
Password: os.Getenv("BROKER_PASSWORD"),
Port: getEnvOrDefault("PORT", "8080"),
}
store := NewPostgresStore()
broker := &DatabaseBroker{
config: config,
store: store,
}
credentials := brokerapi.BrokerCredentials{
Username: config.Username,
Password: config.Password,
}
brokerAPI := brokerapi.New(broker, nil, brokerapi.WithBrokerCredentials(credentials))
router := mux.NewRouter()
router.HandleFunc("/v2/catalog", brokerAPI.Catalog).Methods("GET")
router.HandleFunc("/v2/service_instances/{instance_id}", brokerAPI.Provision).Methods("PUT")
router.HandleFunc("/v2/service_instances/{instance_id}", brokerAPI.Deprovision).Methods("DELETE")
router.HandleFunc("/v2/service_instances/{instance_id}/service_bindings/{binding_id}", brokerAPI.Bind).Methods("PUT")
router.HandleFunc("/v2/service_instances/{instance_id}/service_bindings/{binding_id}", brokerAPI.Unbind).Methods("DELETE")
router.HandleFunc("/v2/service_instances/{instance_id}", brokerAPI.Update).Methods("PATCH")
router.HandleFunc("/v2/service_instances/{instance_id}/last_operation", brokerAPI.LastOperation).Methods("GET")
log.Printf("Starting broker on port %s", config.Port)
log.Fatal(http.ListenAndServe(":"+config.Port, router))
}
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
Service Catalog Implementation
// catalog.go
package main
import (
"context"
"github.com/pivotal-cf/brokerapi/v8/domain"
"github.com/pivotal-cf/brokerapi/v8/domain/apiresponses"
)
func (b *DatabaseBroker) Services(ctx context.Context) ([]domain.Service, error) {
return []domain.Service{
{
ID: "postgresql-service-id",
Name: "postgresql",
Description: "PostgreSQL Database Service",
Bindable: true,
PlanUpdatable: true,
Tags: []string{"postgresql", "database", "relational"},
Metadata: &domain.ServiceMetadata{
DisplayName: "PostgreSQL",
ImageUrl: "https://example.com/postgresql-icon.png",
LongDescription: "Managed PostgreSQL database instances with automated backups and high availability",
ProviderDisplayName: "Company Platform Team",
DocumentationUrl: "https://docs.company.com/postgresql",
SupportUrl: "https://support.company.com",
},
Plans: []domain.ServicePlan{
{
ID: "postgresql-small",
Name: "small",
Description: "Small PostgreSQL instance (2 CPU, 4GB RAM, 20GB storage)",
Free: boolPtr(false),
Bindable: boolPtr(true),
Metadata: &domain.ServicePlanMetadata{
DisplayName: "Small",
Bullets: []string{
"2 CPU cores",
"4GB RAM",
"20GB storage",
"Automated backups",
"99.9% SLA",
},
Costs: []domain.ServicePlanCost{
{
Amount: map[string]float64{"usd": 50.00},
Unit: "MONTHLY",
},
},
},
Schemas: &domain.ServiceSchemas{
Instance: domain.ServiceInstanceSchema{
Create: domain.Schema{
Parameters: map[string]interface{}{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": map[string]interface{}{
"backup_retention_days": map[string]interface{}{
"type": "integer",
"minimum": 1,
"maximum": 35,
"default": 7,
"description": "Number of days to retain backups",
},
"high_availability": map[string]interface{}{
"type": "boolean",
"default": false,
"description": "Enable multi-AZ deployment",
},
"encryption": map[string]interface{}{
"type": "boolean",
"default": true,
"description": "Enable encryption at rest",
},
},
},
},
Update: domain.Schema{
Parameters: map[string]interface{}{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": map[string]interface{}{
"backup_retention_days": map[string]interface{}{
"type": "integer",
"minimum": 1,
"maximum": 35,
},
},
},
},
},
Binding: domain.ServiceBindingSchema{
Create: domain.Schema{
Parameters: map[string]interface{}{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": map[string]interface{}{
"privileges": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{
"type": "string",
"enum": []string{"READ", "WRITE", "ADMIN"},
},
"default": []string{"READ", "WRITE"},
},
},
},
},
},
},
},
{
ID: "postgresql-medium",
Name: "medium",
Description: "Medium PostgreSQL instance (4 CPU, 16GB RAM, 100GB storage)",
Free: boolPtr(false),
Bindable: boolPtr(true),
Metadata: &domain.ServicePlanMetadata{
DisplayName: "Medium",
Bullets: []string{
"4 CPU cores",
"16GB RAM",
"100GB storage",
"Automated backups",
"High availability",
"99.95% SLA",
},
Costs: []domain.ServicePlanCost{
{
Amount: map[string]float64{"usd": 200.00},
Unit: "MONTHLY",
},
},
},
},
{
ID: "postgresql-large",
Name: "large",
Description: "Large PostgreSQL instance (8 CPU, 32GB RAM, 500GB storage)",
Free: boolPtr(false),
Bindable: boolPtr(true),
Metadata: &domain.ServicePlanMetadata{
DisplayName: "Large",
Bullets: []string{
"8 CPU cores",
"32GB RAM",
"500GB storage",
"Automated backups",
"High availability",
"Performance Insights",
"99.99% SLA",
},
Costs: []domain.ServicePlanCost{
{
Amount: map[string]float64{"usd": 800.00},
Unit: "MONTHLY",
},
},
},
},
},
},
{
ID: "redis-service-id",
Name: "redis",
Description: "Redis Cache Service",
Bindable: true,
PlanUpdatable: true,
Tags: []string{"redis", "cache", "in-memory"},
Metadata: &domain.ServiceMetadata{
DisplayName: "Redis",
ImageUrl: "https://example.com/redis-icon.png",
LongDescription: "Managed Redis cache instances with clustering and persistence options",
ProviderDisplayName: "Company Platform Team",
},
Plans: []domain.ServicePlan{
{
ID: "redis-small",
Name: "small",
Description: "Small Redis instance (1GB memory)",
Free: boolPtr(false),
Bindable: boolPtr(true),
},
{
ID: "redis-medium",
Name: "medium",
Description: "Medium Redis instance (5GB memory, replication)",
Free: boolPtr(false),
Bindable: boolPtr(true),
},
},
},
}, nil
}
func boolPtr(b bool) *bool {
return &b
}
Provisioning Implementation
// provision.go
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/pivotal-cf/brokerapi/v8/domain"
"github.com/pivotal-cf/brokerapi/v8/domain/apiresponses"
)
type ProvisionParameters struct {
BackupRetentionDays int `json:"backup_retention_days"`
HighAvailability bool `json:"high_availability"`
Encryption bool `json:"encryption"`
}
func (b *DatabaseBroker) Provision(ctx context.Context, instanceID string, details domain.ProvisionDetails, asyncAllowed bool) (domain.ProvisionedServiceSpec, error) {
// Parse parameters
var params ProvisionParameters
if len(details.RawParameters) > 0 {
if err := json.Unmarshal(details.RawParameters, ¶ms); err != nil {
return domain.ProvisionedServiceSpec{}, apiresponses.ErrRawParamsInvalid
}
}
// Set defaults
if params.BackupRetentionDays == 0 {
params.BackupRetentionDays = 7
}
if !params.Encryption {
params.Encryption = true
}
// Check if instance already exists
exists, err := b.store.InstanceExists(ctx, instanceID)
if err != nil {
return domain.ProvisionedServiceSpec{}, err
}
if exists {
return domain.ProvisionedServiceSpec{}, apiresponses.ErrInstanceAlreadyExists
}
// Create instance asynchronously
if asyncAllowed {
// Start provisioning in background
go func() {
if err := b.provisionInstance(context.Background(), instanceID, details.ServiceID, details.PlanID, params); err != nil {
// Log error and update status
fmt.Printf("Failed to provision instance %s: %v\n", instanceID, err)
}
}()
return domain.ProvisionedServiceSpec{
IsAsync: true,
OperationData: "provisioning",
DashboardURL: fmt.Sprintf("https://console.company.com/databases/%s", instanceID),
}, nil
}
// Synchronous provisioning
if err := b.provisionInstance(ctx, instanceID, details.ServiceID, details.PlanID, params); err != nil {
return domain.ProvisionedServiceSpec{}, err
}
return domain.ProvisionedServiceSpec{
IsAsync: false,
DashboardURL: fmt.Sprintf("https://console.company.com/databases/%s", instanceID),
}, nil
}
func (b *DatabaseBroker) provisionInstance(ctx context.Context, instanceID, serviceID, planID string, params ProvisionParameters) error {
// Store instance metadata
instance := ServiceInstance{
ID: instanceID,
ServiceID: serviceID,
PlanID: planID,
BackupRetentionDays: params.BackupRetentionDays,
HighAvailability: params.HighAvailability,
Encryption: params.Encryption,
Status: "creating",
}
if err := b.store.SaveInstance(ctx, instance); err != nil {
return err
}
// Provision actual database (AWS RDS, Azure Database, etc.)
dbInstance, err := b.provisionDatabase(ctx, instanceID, planID, params)
if err != nil {
instance.Status = "failed"
instance.StatusMessage = err.Error()
b.store.SaveInstance(ctx, instance)
return err
}
// Update instance with connection details
instance.Endpoint = dbInstance.Endpoint
instance.Port = dbInstance.Port
instance.Status = "available"
return b.store.SaveInstance(ctx, instance)
}
func (b *DatabaseBroker) provisionDatabase(ctx context.Context, instanceID, planID string, params ProvisionParameters) (*DatabaseInstance, error) {
// Implementation depends on cloud provider
// Example for AWS RDS, Azure Database, etc.
return &DatabaseInstance{
Endpoint: fmt.Sprintf("%s.db.company.com", instanceID),
Port: 5432,
}, nil
}
Binding Implementation
// bind.go
package main
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/pivotal-cf/brokerapi/v8/domain"
"github.com/pivotal-cf/brokerapi/v8/domain/apiresponses"
)
type BindingCredentials struct {
URI string `json:"uri"`
Username string `json:"username"`
Password string `json:"password"`
Host string `json:"host"`
Port int `json:"port"`
Database string `json:"database"`
}
func (b *DatabaseBroker) Bind(ctx context.Context, instanceID, bindingID string, details domain.BindDetails, asyncAllowed bool) (domain.Binding, error) {
// Get instance
instance, err := b.store.GetInstance(ctx, instanceID)
if err != nil {
return domain.Binding{}, apiresponses.ErrInstanceDoesNotExist
}
// Check if binding already exists
exists, err := b.store.BindingExists(ctx, instanceID, bindingID)
if err != nil {
return domain.Binding{}, err
}
if exists {
return domain.Binding{}, apiresponses.ErrBindingAlreadyExists
}
// Create database user and credentials
username := fmt.Sprintf("user_%s", bindingID[:8])
password := generatePassword(32)
if err := b.createDatabaseUser(ctx, instance, username, password); err != nil {
return domain.Binding{}, err
}
// Store binding
binding := ServiceBinding{
ID: bindingID,
InstanceID: instanceID,
Username: username,
Password: password,
}
if err := b.store.SaveBinding(ctx, binding); err != nil {
return domain.Binding{}, err
}
// Return credentials
credentials := BindingCredentials{
URI: fmt.Sprintf("postgresql://%s:%s@%s:%d/%s", username, password, instance.Endpoint, instance.Port, instanceID),
Username: username,
Password: password,
Host: instance.Endpoint,
Port: instance.Port,
Database: instanceID,
}
return domain.Binding{
Credentials: credentials,
}, nil
}
func (b *DatabaseBroker) Unbind(ctx context.Context, instanceID, bindingID string, details domain.UnbindDetails, asyncAllowed bool) (domain.UnbindSpec, error) {
// Get binding
binding, err := b.store.GetBinding(ctx, instanceID, bindingID)
if err != nil {
return domain.UnbindSpec{}, apiresponses.ErrBindingDoesNotExist
}
// Get instance
instance, err := b.store.GetInstance(ctx, instanceID)
if err != nil {
return domain.UnbindSpec{}, err
}
// Delete database user
if err := b.deleteDatabaseUser(ctx, instance, binding.Username); err != nil {
return domain.UnbindSpec{}, err
}
// Delete binding
if err := b.store.DeleteBinding(ctx, instanceID, bindingID); err != nil {
return domain.UnbindSpec{}, err
}
return domain.UnbindSpec{}, nil
}
func generatePassword(length int) string {
bytes := make([]byte, length)
rand.Read(bytes)
return base64.URLEncoding.EncodeToString(bytes)[:length]
}
func (b *DatabaseBroker) createDatabaseUser(ctx context.Context, instance ServiceInstance, username, password string) error {
// Implementation depends on database type
// Execute SQL: CREATE USER, GRANT privileges
return nil
}
func (b *DatabaseBroker) deleteDatabaseUser(ctx context.Context, instance ServiceInstance, username string) error {
// Implementation depends on database type
// Execute SQL: REVOKE privileges, DROP USER
return nil
}
Kubernetes Integration
Service Catalog Installation
# Install Service Catalog
apiVersion: v1
kind: Namespace
metadata:
name: service-catalog
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: service-catalog
namespace: service-catalog
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: service-catalog
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: service-catalog
namespace: service-catalog
Broker Registration
# Register broker with Service Catalog
apiVersion: servicecatalog.k8s.io/v1beta1
kind: ClusterServiceBroker
metadata:
name: database-broker
spec:
url: https://database-broker.platform.svc.cluster.local
authInfo:
basic:
secretRef:
name: broker-credentials
namespace: platform
caBundle: LS0tLS1CRUdJTi... # base64 encoded CA cert
Service Instance Creation
# Create service instance
apiVersion: servicecatalog.k8s.io/v1beta1
kind: ServiceInstance
metadata:
name: my-postgres-db
namespace: my-app
spec:
clusterServiceClassExternalName: postgresql
clusterServicePlanExternalName: medium
parameters:
backup_retention_days: 14
high_availability: true
encryption: true
---
# Create service binding
apiVersion: servicecatalog.k8s.io/v1beta1
kind: ServiceBinding
metadata:
name: my-postgres-binding
namespace: my-app
spec:
instanceRef:
name: my-postgres-db
secretName: postgres-credentials
Application Usage
# Application using bound service
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:latest
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: postgres-credentials
key: uri
- name: DB_HOST
valueFrom:
secretKeyRef:
name: postgres-credentials
key: host
- name: DB_PORT
valueFrom:
secretKeyRef:
name: postgres-credentials
key: port
- name: DB_NAME
valueFrom:
secretKeyRef:
name: postgres-credentials
key: database
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: postgres-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-credentials
key: password
Best Practices
Security
- Authentication: Require basic auth or OAuth for broker API
- Encryption: Use TLS for all communications
- Credential Rotation: Support password rotation
- Least Privilege: Grant minimal database permissions
- Audit Logging: Log all provisioning operations
Reliability
- Async Operations: Use async mode for long-running operations
- Idempotency: Support retries safely
- Status Tracking: Implement LastOperation endpoint
- Error Handling: Provide detailed error messages
- Health Checks: Expose readiness and liveness probes
Operational Excellence
- Monitoring: Export Prometheus metrics
- Logging: Structured logging with context
- Documentation: Comprehensive service catalog metadata
- Cost Tracking: Include pricing information
- SLA Definition: Document availability guarantees
Conclusion
Open Service Broker API enables standardized service provisioning across platforms. Key benefits include:
- Self-Service: Developers provision resources on-demand
- Standardization: Consistent interface across service types
- Automation: Reduce manual provisioning workflows
- Governance: Centralized control and auditing
- Multi-Cloud: Abstract cloud provider differences
Success requires robust implementation, comprehensive testing, and strong operational practices for managing the broker infrastructure.