📝 BLOG.IK.AM

@making's memo
(🗃 Categories 🏷 Tags)

Pivotal Container Service (PKS)にService Catalogをインストールするためのworkaround

🗃 {Dev/CaaS/Kubernetes/PKS}

🏷 Kubernetes 🏷 PKS 🏷 Service Catalog 🏷 Open Service Broker API 🏷 BOSH 🏷 CFCR

🗓 Updated at 2019-04-02T03:22:27+09:00 by Toshiaki Maki  🗓 Created at 2019-01-06T18:33:35+09:00 by Toshiaki Maki  {✒️️ Edit  ⏰ History}


️ 【Updated】

以下のバージョンで再確認したところ、特に追加設定不要でService Catalogをインストールできました。

  • Pivotal Container Service 1.3.5
  • Ops Manager 2.4.7
  • Kubernetes 1.12.7
  • Service Catalog 0.1.42

下記の作業は行わないでください

以前k8s meetupで話したService Catalogですが、執筆現時点ではPKS上で使うにはちょっとしたhackを行う必要があったので、
その方法をメモします。CFCRでも基本的には同じです。

以前の資料はこちら。

Open Service Broker APIとKubernetes Service Catalog #k8sjp from Toshiaki Maki

以下の内容は次のバージョンで確認しました。

  • Pivotal Container Service 1.2.6
  • Ops Manager 2.3.7
  • Kubernetes 1.11.6
  • Service Catalog 0.1.38

️: 本記事の内容はPivotalが推奨する方法ではありません。試す場合は自己責任で行い、本内容で生じる問題をサポートに問い合わせないでください。

目次

Helmのインストール

Helmは好みではないのですが、現状Service CatalogをインストールするにはHelmを使うのが一番簡単なのでこの記事ではHelmを使用します。
Helmインストール済みの場合はskipしてください。

まずはTillerをデプロイするためのServiceAccoutClusterRoleBindingの設定をします。

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: tiller
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: tiller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
  - kind: ServiceAccount
    name: tiller
    namespace: kube-system
EOF

helm CLIをインストールします。

brew install kubernetes-helm

Tillerをインストールします。

helm init --service-account tiller

Service Catalogのインストール

Service Catalog用のレポジトリを追加します。

helm repo add svc-cat https://svc-catalog-charts.storage.googleapis.com

本記事ではバージョン0.1.38を使用します。

$ helm search service-catalog
NAME            CHART VERSION APP VERSION DESCRIPTION                                                 
svc-cat/catalog 0.1.38                    service-catalog API server and controller-manager helm chart

catalog namespaceにService Catalogをインストールします。

helm install svc-cat/catalog \
    --name catalog --namespace catalog

Workaroundの一時適用

実はPKS上ではこのままでは動きません。catalog-controller-managerがCrashするはずです。次のようなログが出力されます。

W0105 15:09:48.062514       1 feature_gate.go:198] Setting GA feature gate OriginatingIdentity=true. It will be removed in a future release.
I0105 15:09:48.062663       1 feature_gate.go:206] feature gates: &{map[OriginatingIdentity:true]}
I0105 15:09:48.062747       1 feature_gate.go:206] feature gates: &{map[OriginatingIdentity:true ServicePlanDefaults:false]}
I0105 15:09:48.062813       1 hyperkube.go:192] Service Catalog version v0.1.38 (built 2018-11-09T01:46:26Z)
I0105 15:09:48.062905       1 controller_manager.go:105] Building k8s kubeconfig
I0105 15:09:48.065019       1 controller_manager.go:133] Building service-catalog kubeconfig for url: 
I0105 15:09:48.065067       1 controller_manager.go:140] Using inClusterConfig to talk to service catalog API server -- make sure your API server is registered with the aggregator
I0105 15:09:48.065286       1 controller_manager.go:161] Starting http server and mux
I0105 15:09:48.065399       1 controller_manager.go:193] Creating event broadcaster
I0105 15:09:48.065553       1 healthz.go:115] Installing healthz checkers:"ping", "checkAPIAvailableResources"
I0105 15:09:48.065634       1 metrics.go:92] Registered /metrics with prometheus
I0105 15:09:48.066040       1 controller_manager.go:295] Created client for API discovery
I0105 15:09:48.066181       1 round_trippers.go:386] curl -k -v -XGET  -H "User-Agent: service-catalog/v0.1.38 (linux/amd64) kubernetes/19c1ae7/service-catalog-controller-discovery" -H "Accept: application/json, */*" 'https://10.100.200.1:443/apis/servicecatalog.k8s.io/v1beta1?timeout=32s'
I0105 15:09:48.166743       1 round_trippers.go:405] GET https://10.100.200.1:443/apis/servicecatalog.k8s.io/v1beta1?timeout=32s 401 Unauthorized in 100 milliseconds
I0105 15:09:48.166765       1 round_trippers.go:411] Response Headers:
I0105 15:09:48.166772       1 round_trippers.go:414]     Audit-Id: 0312d47a-c9b8-47cc-ba38-76677eaccd5d
I0105 15:09:48.166778       1 round_trippers.go:414]     Content-Type: application/json
I0105 15:09:48.166789       1 round_trippers.go:414]     Date: Sat, 05 Jan 2019 15:09:48 GMT
I0105 15:09:48.166795       1 round_trippers.go:414]     Content-Length: 129
I0105 15:09:48.166839       1 request.go:942] Response Body: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}
F0105 15:09:48.167361       1 controller_manager.go:233] error running controllers: failed to get api versions from server: failed to get supported resources from server: Unauthorized

Service Catalogは現時点ではAPIの提供にCustom Resource Definition(CRD)ではなくAggregation Layerを使用しており、Mutual TLSで認証されています。

Service CatalogのドキュメントのBefore you beginに次のように書かれています。

Note: There are a few setup requirements for getting the aggregation layer working in your environment to support mutual TLS auth between the proxy and extension apiservers. Kubernetes and the kube-apiserver have multiple CAs, so make sure that the proxy is signed by the aggregation layer CA and not by something else, like the master CA.

PKSで作成されたk8sクラスタのAPI Serverに設定されている起動オプションは次のコマンドで確認できます。

bosh -d service-instance_<instance uuid> ssh master -c "cat /var/vcap/jobs/kube-apiserver/config/bpm.yml"

<instance_uuid>pks cluster <cluster_name>またはbosh deploymentsで確認できます。

次のような出力結果になります。

processes:
- name: kube-apiserver
  executable: /var/vcap/packages/kubernetes/bin/kube-apiserver
  args:
  - --anonymous-auth=true
  - --allow-privileged=false
  - --apiserver-count=1
  - --bind-address=0.0.0.0
  - --cloud-provider=vsphere
  - --cloud-config=/var/vcap/jobs/kube-apiserver/config/cloud-provider.ini
  - --kubelet-client-key=/var/vcap/jobs/kube-apiserver/config/
  - --kubelet-client-certificate=/var/vcap/jobs/kube-apiserver/config/
  - --enable-admission-plugins=LimitRanger,DefaultTolerationSeconds,ValidatingiceAccount,DefaultStorageClass,MutatingAdmissionWebhook,DenyEscalatingExec,SecurityContextDeny
  - --disable-admission-plugins=PersistentVolumeLabel
  - --enable-bootstrap-token-auth
  - --enable-swagger-ui
  - --etcd-servers=https://master-0.etcd.cfcr.internal:2379
  - --etcd-cafile=/var/vcap/jobs/kube-apiserver/config/etcd-ca.crt
  - 
  - --etcd-certfile=/var/vcap/jobs/kube-apiserver/config/etcd-client.crt
  - --etcd-keyfile=/var/vcap/jobs/kube-apiserver/config/etcd-client.key
  - --kubelet-https
  - --secure-port=8443
  - --service-account-key-file=/var/vcap/jobs/kube-apiserver/config/
  - --service-account-lookup
  - --service-cluster-ip-range=10.100.200.0/24
  - --service-node-port-range=30000-32767
  - --tls-cert-file=/var/vcap/jobs/kube-apiserver/config/kubernetes.pem
  - --tls-private-key-file=/var/vcap/jobs/kube-apiserver/config/
  - --token-auth-file=/var/vcap/jobs/kube-apiserver/config/tokens.csv
  - --storage-media-type=application/json
  - --enable-aggregator-routing
  - --v=2
  - --authorization-mode=RBAC
  - --runtime-config=api/v1
  - --client-ca-file=/var/vcap/jobs/kube-apiserver/config/kubernetes.pem
  - --proxy-client-cert-file=/var/vcap/jobs/kube-apiserver/config/
  - --proxy-client-key-file=/var/vcap/jobs/kube-apiserver/config/
  - --requestheader-allowed-names=aggregator
  - --requestheader-client-ca-file=/var/vcap/jobs/kube-apiserver/config/
  - --requestheader-extra-headers-prefix=X-Remote-Extra-
  - --requestheader-group-headers=X-Remote-Group
  - --requestheader-username-headers=X-Remote-User
  - --audit-log-path=/var/vcap/sys/log/kube-apiserver/audit.log
  - --audit-log-maxage=0
  - --audit-log-maxsize=49
  - --audit-log-maxbackup=7
  - --audit-policy-file=/var/vcap/jobs/kube-apiserver/config/audit_policy.yml
  - --oidc-issuer-url=https://api.pks.ik.am:8443/oauth/token
  - --oidc-client-id=pks_cluster_client
  - --oidc-username-claim=user_name
  - "--oidc-username-prefix=-"
  - --oidc-groups-claim=roles
  - "--oidc-groups-prefix="
  - --oidc-ca-file=/var/vcap/jobs/kube-apiserver/config/oidc-ca.pem
  env:  

--requestheader-allowed-names=aggregatorがありますが、どうも使われているCAのCNがaggregatorでなく、API Serverのhost名、つまりpks create-cluster <cluster_name> -e <host_name>で指定した<host_name>になっています。
上の例ではdemo.pks.ik.amです。

この場合、--requestheader-allowed-names=aggregator,demo.pks.ik.amにしてAPI Serverを再起動すればうまく動きます。

次のコマンドでrequestheader-allowed-namesの値を一時的に変更します。

bosh -d service-instance_<instance_uuid> ssh master -c "sudo sed -i 's/--requestheader-allowed-names=aggregator$/--requestheader-allowed-names=aggregator,demo.pks.ik.am/g' /var/vcap/jobs/kube-apiserver/config/bpm.yml"

次のコマンドでAPI Serverを再起動します。

bosh -d service-instance_<instance_uuid> ssh master -c "sudo /var/vcap/bosh/bin/monit restart kube-apiserver"

multi-master構成の場合は上記を全てのmasterに対して行います。master/0, master/1, master/2と言うように対象のVMを指定できます。

一旦Service Catalogのpodを削除して、再作成を待ちます。

kubectl -n catalog delete pod -l release=catalog

しばらくするとcatalog-apiserverおよびcatalog-controller-manager

$ kubectl get pod -n catalog 
NAME                                                  READY   STATUS    RESTARTS   AGE
catalog-catalog-apiserver-58dd7744c5-swcmg            2/2     Running   0          17h
catalog-catalog-controller-manager-755b8b54d4-97g7s   1/1     Running   2          17h

将来的にService CatalogがCRDを使う方針に変われば、このWorkaroundは不要になるかもしれません。

CFCR(v0.22.0+)の場合は次のようなops-fileを適用すれば良いです。

- type: replace
  path: /instance_groups/name=master/jobs/name=kube-apiserver/properties/k8s-args/requestheader-allowed-names
  value: aggregator,master.cfcr.internal

Workaroundの恒久適用

上記のようにVMにsshして修正するworkaroundは、一時的にしか効きません。
BOSHはimmutableなinfrastructureを提供するため、updateやrecreateのタイミングで元の設定に戻ります。

workaroundを恒久的に行うにはBOSHのライフサイクルのタイミングに合わせてsedを行うさらなるhackが必要があります。

os-conf-releasepre-start-scriptjobを使うとpre-startのタイミングで任意のscriptを実行することができます。

このpre-start-scriptをMaster VMに無理矢理組み込むためにBOSH Runtime Configが使えます。

次のruntime-configを作成してください。(demo.pks.ik.am<instance_uuid>の部分は変えてください)

cat <<'EOF' > pks-service-catalog-hack.yml 
releases:
- name: os-conf
  version: "20.0.0"
  url: "https://bosh.io/d/github.com/cloudfoundry/os-conf-release?v=20.0.0"
  sha1: "a60187f038d45e2886db9df82b72a9ab5fdcc49d"

addons:
- name: pks-service-catalog-hack_<instance_uuid>
  jobs:
  - name: pre-start-script
    release: os-conf
    properties:
      script: |
        #!/bin/bash
        sed -i 's/--requestheader-allowed-names=aggregator$/--requestheader-allowed-names=aggregator,demo.pks.ik.am/g' /var/vcap/jobs/kube-apiserver/config/bpm.yml
  include:
    deployments:
    - service-instance_<instance_uuid>
    jobs:
    - name: kube-apiserver
      release: kubo
EOF

適用したいクラスタが複数ある場合はaddoins以下の内容をコピー&ペーストして<instance_uuid>を変えてください。

これをPKSのBOSH Directorに適用します。

bosh -n update-runtime-config --name=pks-service-catalog-hack pks-service-catalog-hack.yml --no-redact

その後、Ops Manager上でApply Changesを実行します。その際、"Update all cluster errand"にチェックが入っていることを確認してください。

image

これでクラスタ名が<instance_uuid>であるk8sのmaster vm上に上記のhackを適用できます。vmがrecreateされても大丈夫です。

少なくともPKS 1.2では動作しますが、1.3以降/var/vcap/jobs/kube-apiserver/config/bpm.ymlの内容が変わるとsedの条件が合致しなくなる可能性があります。
その場合はruntime configを修正してupdateする必要があります。

サンプルのデプロイ

サンプルとしてKafka as a ServiceのCloud KarafkaのインスタンスをプロビジョンしてくれるService Brokerをデプロイしてみます。

https://github.com/making/cloud-karafka-service-broker

Cloud Karafkaのアカウントを作成して、API Keyを取得してください。

image

Service Brokerのデプロイ

まずはService Brokerを立ち上げます。

API_KEY=cd129182-9dc3-4de4-85d3-9541af9e9180

kubectl apply -f https://github.com/making/cloud-karafka-service-broker/raw/0.1.0/k8s/namespace.yml

kubectl create -n osb secret generic cloudkarafka-service-broker-secret \
  --from-literal=username=admin\
  --from-literal=password=password \
  --from-literal=cloudkarafka-api-key=${API_KEY} \
  --dry-run -o yaml > secret.yml
kubectl apply -f secret.yml
kubectl apply -f https://github.com/making/cloud-karafka-service-broker/raw/0.1.0/k8s/deployment.yml

次にClusterServiceBrokerリソースを作成します。次のようなmanifestになります。

apiVersion: servicecatalog.k8s.io/v1beta1
kind: ClusterServiceBroker
metadata:
  name: cloudkarafka
spec:
  url: http://cloudkarafka-service-broker-service.osb.svc.cluster.local
  authInfo:
    basic:
      secretRef:
        name: cloudkarafka-service-broker-secret
        namespace: osb

デプロイします。

kubectl apply -f https://github.com/making/cloud-karafka-service-broker/raw/0.1.0/k8s/cluster-service-broker.yml

ClusterServiceClassリソースとClusterServicePlanリソースを確認します。

$ kubectl get clusterserviceclasses -o custom-columns=CLASS:.spec.externalName,DESCRIPTION:.spec.description
CLASS          DESCRIPTION
cloudkarafka   CloudKarafka

$ kubectl get clusterserviceplans -o custom-columns=CLASS:.spec.clusterServiceBrokerName,PLAN:.spec.externalName,DESCRIPTION:.spec.description
CLASS          PLAN   DESCRIPTION
cloudkarafka   duck   Developer Duck

ユーティリティCLIとしてsvcat CLIをインストールします。

brew install kubernetes-service-catalog-client

同じくClusterServiceClassリソースとClusterServicePlanリソースを確認します

$ svcat get classes
      NAME       NAMESPACE   DESCRIPTION   
+--------------+-----------+--------------+
  cloudkarafka               CloudKarafka  

$ svcat get plans

  NAME   NAMESPACE      CLASS        DESCRIPTION    
+------+-----------+--------------+----------------+
  duck               cloudkarafka   Developer Duck  

$ svcat marketplace
     CLASS       PLANS   DESCRIPTION   
+--------------+-------+--------------+
  cloudkarafka   duck    CloudKarafka 

Serice Instanceの作成

Karafkaサービスの実インスタンスとなるServiceInstanceリソースを作成します。次のようなmanifestになります。

apiVersion: servicecatalog.k8s.io/v1beta1
kind: ServiceInstance
metadata:
  name: demo-kafka
spec:
  clusterServiceClassExternalName: cloudkarafka
  clusterServicePlanExternalName: duck

デプロイします。

kubectl apply -f https://github.com/making/cloud-karafka-service-broker/raw/0.1.0/k8s/sample/service-instance.yml

ServiceInstance一覧を確認します。

$ kubectl get serviceinstances -n default
NAME         CLASS                              PLAN   STATUS   AGE
demo-kafka   ClusterServiceClass/cloudkarafka   duck   Ready    46s

kubectl describeで詳細を確認します。

$ kubectl describe serviceinstance -n default demo-kafka
Name:         demo-kafka
Namespace:    default
Labels:       <none>
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"servicecatalog.k8s.io/v1beta1","kind":"ServiceInstance","metadata":{"annotations":{},"name":"demo-kafka","namespace":"defau...
API Version:  servicecatalog.k8s.io/v1beta1
Kind:         ServiceInstance
Metadata:
  Creation Timestamp:  2019-01-06T11:52:16Z
  Finalizers:
    kubernetes-incubator/service-catalog
  Generation:        1
  Resource Version:  248
  Self Link:         /apis/servicecatalog.k8s.io/v1beta1/namespaces/default/serviceinstances/demo-kafka
  UID:               83b979b6-11a9-11e9-9bad-8a484bfab1d8
Spec:
  Cluster Service Class External Name:  cloudkarafka
  Cluster Service Class Ref:
    Name:                              d21df959-6da8-495f-a8b6-ddc51d49eea2
  Cluster Service Plan External Name:  duck
  Cluster Service Plan Ref:
    Name:           e64a8b50-be7f-48cf-9010-9ccb7fe075b5
  External ID:      83b97931-11a9-11e9-9bad-8a484bfab1d8
  Update Requests:  0
  User Info:
    Groups:
      administrators
      users
      system:authenticated
    UID:       
    Username:  toshiaki
Status:
  Async Op In Progress:  false
  Conditions:
    Last Transition Time:  2019-01-06T11:52:19Z
    Message:               The instance was provisioned successfully
    Reason:                ProvisionedSuccessfully
    Status:                True
    Type:                  Ready
  Dashboard URL:           https://customer.cloudkarafka.com/instance/6980/sso
  Deprovision Status:      Required
  External Properties:
    Cluster Service Plan External ID:    e64a8b50-be7f-48cf-9010-9ccb7fe075b5
    Cluster Service Plan External Name:  duck
    User Info:
      Groups:
        administrators
        users
        system:authenticated
      UID:                        
      Username:                   toshiaki
  Observed Generation:            1
  Orphan Mitigation In Progress:  false
  Provision Status:               Provisioned
  Reconciled Generation:          1
Events:
  Type    Reason                   Age    From                                Message
  ----    ------                   ----   ----                                -------
  Normal  ProvisionedSuccessfully  2m51s  service-catalog-controller-manager  The instance was provisioned successfully

manifestの代わりにsvcatで作ることもできます。

svcat provision -n default --class cloudkarafka demo-kafka --plan duck

svcatを使ってService Instanceの詳細を確認します。

$ svcat -n default describe instance demo-kafka
  Name:           demo-kafka                                                                         
  Namespace:      default                                                                            
  Status:         Ready - The instance was provisioned successfully @ 2019-01-06 11:52:19 +0000 UTC  
  DashboardURL:   https://customer.cloudkarafka.com/instance/6980/sso                                
  Class:          cloudkarafka                                                                       
  Plan:           duck                                                                               

Parameters:
  No parameters defined

Bindings:
No bindings defined

Cloud Karafkaのインスタンス一覧を確認すると、インスタンスが作成されていることがわかります。

image

インスタンス名にService InstanceのExternal IDが使われています。

Service Bindingの作成

次にServiceBindingリソースを作成します。次のようなmanifestになります。

apiVersion: servicecatalog.k8s.io/v1beta1
kind: ServiceBinding
metadata:
  name: demo-kafka-binding
spec:
  instanceRef:
    name: demo-kafka
  secretName: demo-kafka-secret

デプロイします。

kubectl apply -f https://github.com/making/cloud-karafka-service-broker/raw/0.1.0/k8s/sample/service-binding.yml

ServiceBinding一覧を確認します。

$ kubectl get servicebindings -n default
NAME                 SERVICE-INSTANCE   SECRET-NAME         STATUS   AGE
demo-kafka-binding   demo-kafka         demo-kafka-secret   Ready    6m

demo-kafka-secretというSecretリソースが作成されています。

$ kubectl describe secret -n default demo-kafka-secret 
Name:         demo-kafka-secret
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
password:     32 bytes
topicPrefix:  9 bytes
username:     8 bytes
brokers:      107 bytes
ca:           2086 bytes
name:         39 bytes

サンプルアプリのデプロイ

Kafka Producerをデプロイします。

kubectl apply -f https://github.com/making/cloud-karafka-service-broker/raw/0.1.0/k8s/sample/producer.yml

Kafka Consumerをデプロイします。

kubectl apply -f https://github.com/making/cloud-karafka-service-broker/raw/0.1.0/k8s/sample/consumer.yml

ServiceはNodePortで公開されています。

$ kubectl get service,pod -n default
NAME                                  TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/demo-kafka-consumer-service   NodePort    10.100.200.113   <none>        80:31876/TCP   6m
service/demo-kafka-producer-service   NodePort    10.100.200.57    <none>        80:30776/TCP   19m
service/kubernetes                    ClusterIP   10.100.200.1     <none>        443/TCP        1d

NAME                                       READY   STATUS    RESTARTS   AGE
pod/demo-kafka-consumer-dd94c966b-jsgv6    1/1     Running   0          6m
pod/demo-kafka-producer-7b44d795d4-drg89   1/1     Running   0          19m

Consumerにアクセスします。Server-Sent Eventでメッセージが流れます。

curl <worker_ip>:31876/messages

Producerにメッセージを送ります。

curl <worker_ip>:30776/messages -H Content-Type:text/plain -d Hello
curl <worker_ip>:30776/messages -H Content-Type:text/plain -d Hello2

Consumer側にメッセージが流れます。

data:Hello

data:Hello2

これで次のようなデータの流れが実現できました。

Producer -> [Cloud Karafka] -> Consumer

Cloud KarafkaのインスタンスはService Broker経由でServiceInstanceリソースとして作成され、
アクセスするためにCredentials情報はServiceBindingリソースで作成されSecretに保存されています。

これでDeveloperがCloud Karafkaのコンソールでインスタンスを作成してCredentialsをコピーしてSecretを手で作成しなくて済みます。

次はAWS Service Brokerを試したい。