Manual Kubernetes Deployment

Instruction set for deploying Kubernetes via Rancher on OpenStack using the out-of tree cloud control manager.

Preface

Since a few years, Kubernetes is trying to move away from in-tree support for cloud provider to out-of-tree or external support. See https://kubernetes.io/blog/2019/04/17/the-future-of-cloud-providers-in-kubernetes/ This essentially means that instead of containing the code for interacting with a given cloud provider in the main Kubernetes repository, the cloud controller manager takes over this task instead. This controller can then be expanded with a plugin to handle a respective cloud provider. OpenStack in-tree support has recently been deprecated such that future deployments of Kubernetes have to use the out-of-tree approach.

The code for the official OpenStack plugin for the cloud control manager can be found here: https://github.com/kubernetes/cloud-provider-openstack Furthermore, the configurations shown later in this guide are either based on the official OpenStack plugin or on this repo https://github.com/rootsami/terraform-rancher2

These instructions were tested on both Rancher2.5 and Rancher2.6 for Kubernetes 1.20 and 1.23.

Preparation

This must be done once and can be used to deploy any number of Kubernetes cluster.

Kubectl must be installed for later configuration of the cluster.

(Optional) Setup OpenStack CLI

The OpenStack CLI can be used instead of the Horizon web interface for configuring OpenStack. https://github.com/openstack/python-openstackclient

Install via pip

pip install python-openstackclient

Set the required variables

export OS_AUTH_URL="https://api.prod.cloud.gwdg.de:5000/v3"
export OS_IDENTITY_API_VERSION= "3"
export OS_PROJECT_NAME=
export OS_PROJECT_DOMAIN_NAME= "GWDG"
export OS_USERNAME=
export OS_USER_DOMAIN_NAME= "GWDG"
export OS_PASSWORD=

Project name is the name of the OpenStack project that is shown at the top when logged in to OpenStack horizon. Username and password are your username and password used for logging in to OpenStack. If password is not provided, the CLI will query it for every command. When using export to set the password make sure that the machine is secure or that the entry is removed from the shell history.

Enable OpenStack Node Driver

The OpenStack Node Driver must be enabled in Rancher for the node deployment to work.

Rancher2.5

In the web UI -> Tools -> Drivers -> Node Drivers -> Select OpenStack and activate

Rancher2.6

In the web UI -> Open Left sidebar -> Cluster Management -> Drivers -> Node Drivers -> Select OpenStack and activate it

Security group

OpenStack web UI -> Network -> Security Groups -> Create a new group called “k8s-node” -> Manage its rules and add the following rules. CIDR should always be set as 10.254.1.0/24 as this is the internal IP range used by OpenStack for its nodes and it prevents external access. This security group should also be added to the Rancher server hosts. If Rancher is not hosted via OpenStack on this IP range, add all rules again with the CIDR of the Rancher server. Rules:

Protocol

Port(s)

TCP

22 (SSH)

TCP

80 (HTTP)

TCP

443 (HTTPS)

TCP

2376

TCP

2379

TCP

2380

TCP

6443

TCP

6783

TCP

6784

TCP

8443

TCP

8472

TCP

9099

TCP

9100

TCP

9433

TCP

9913

TCP

10248

TCP

10250

TCP

10254

TCP

30000-32767

UDP

6783

UDP

6784

UDP

8443

UDP

8472

UDP

30000-32767

This prevents future headaches by opening all ports that Rancher might need according to its documentation: https://rancher.com/docs/rancher/v2.6/en/installation/requirements/ports/

via CLI

openstack security group create k8s-node
openstack security group rule create --remote-ip 10.254.1.0/24 --dst-port PORT --protocol PROTOCOL k8s-node

Insert for PORT the number specified above. For port ranges use start:end. For PROTOCOL insert either TCP or UDP as specified in the table.

Node Template

Find the following values in the OpenStack web UI or via the CLI and note them down.

flavorName:

OpenStack web UI -> Compute -> Instances -> Launch Instance -> Flavor -> choose one and note down the name, for example: ‘m1.medium’

Or via CLI -> openstack flavor list

imageName:

OpenStack web UI -> Compute -> Images -> choose one, for example: ‘Ubuntu 20.04.3 Server x86_64 (ssd)’

Or via CLI -> openstack image list

netId:

OpenStack web UI -> Network -> select Networks -> pick a private network -> select overview -> ID

Or via CLI -> openstack network list -> private network ID

password:

The password for your OpenStack account you use to login.

tenantId:

OpenStack web UI -> Identity -> Projects -> Project ID

Or via CLI -> openstack project list -> ID

username:

The username for your OpenStack account you sue to login.

Node template

Rancher2.5

Rancher web UI -> click profile picture in top right -> Node Templates -> Add Template -> OpenStack -> fill the template according to the following template while filling in the values gathered above

Rancher2.6

In the web UI -> Open Left sidebar -> Cluster Management -> REK1 Configuration -> Node Templates -> Add Template -> OpenStack -> fill the template according to the following template while filling in the values gathered above

Node template template

authURL: https://api.prod.cloud.gwdg.de:5000/v3
domainName: GWDG
endpointType: publicURL
flavorName:
imageName:
netId:
password:
region: RegionOne
secGroups: k8s-node,default
sshUser: cloud
tenantDomainName: GWDG
tenantId:
username:
volumeName: rancher-volume
volumeSize: 0
volumeType: ssd

Kubernetes Cluster Deployment

Kubernetes Cluster deployment on OpenStack using Rancher.

Cluster Creation

Rancher2.5

Rancher web UI -> Global context -> Add Cluster -> OpenStack

Rancher2.6

Rancher web UI -> Home -> Create -> OpenStack

Set a name for the cluster. Add at least one node pool using the node template created above. Set for at least one node pool control plane and etcd to active. Give the node a fitting name. This will be used as a prefix for nodes of this pool, for example CLUSTERNAME-master and CLUSTERNAME-worker, where CLUSTERNAME is the name of the Kubernetes cluster you are about to create.

Under Kubernetes Options select the desired Kubernetes Version and under Cloud Provider select External.

Press create. This will create node instance on OpenStack using the Node template and spin up a Kubernetes cluster. This will take some time, usually 10 to 15 min.

When viewed in Rancher, all nodes with be marked with a taint “uninitialized=NoSchedule”.

To finish initialization, the external cloud control manager for OpenStack must be configured and deployed.

OpenStack cloud control manager

The next steps require kubectl to be installed and working.

Rancher2.5

In the Rancher Web UI -> Select the new cluster -> Top right Kubeconfig File

Rancher2.6

In the Rancher Web UI -> Select the new cluster -> Top right Copy or Download Kubeconfig File

Copy these settings to a file called config and place it under ~/.kube/config on your host. See [[#Managing multiple kubeconfigs]] for how to handle multiple kubeconfigs.

Next gather the details required to fill out the cloud conf file.

username:

Your OpenStack username.

password:

Your OpenStack password.

tenant-id:

Your OpenStack project id, see tenantId above.

subnet-id:

OpenStack Web UI -> Network -> Networks -> private network -> Subnets -> ID

Or via CLI -> openstack subnet list -> ID

floating-network-id:

OpenStack Web UI -> Network -> Networks -> public -> Overview -> ID

Or via CLI -> openstack network list -> public network ID

router-id:

OpenStack Web UI -> Network -> Routers -> select router -> Overview -> ID

Or via CLI -> openstack router list -> ID

Cloud conf

Create a file called cloud.conf and fill it with the template below. Then fill in the open fields as described above.

[Global]
auth-url = https://api.prod.cloud.gwdg.de:5000/v3
username =
password =
region = RegionOne
tenant-id =
domain-name = GWDG

[BlockStorage]
ignore-volume-az = true
trust-device-path = false

[Networking]
public-network-name = public

[LoadBalancer]
use-octavia = false
subnet-id =
floating-network-id =
create-monitor = false
manage-security-groups = true
monitor-max-retries = 0
enabled = true
lb-version = v2
lb-provider = haproxy

[Route]
router-id =

[Metadata]
request-timeout = 0

Afterwards use kubectl to create a secret from this config:

kubectl create secret -n kube-system generic cloud-config --from-file=cloud.conf
Deploying the cloud controller manager

Create yaml files with the following contents:

cloud-controller-manager-role-bindings.yaml

apiVersion: v1
items:
- apiVersion: rbac.authorization.k8s.io/v1
  kind: ClusterRoleBinding
  metadata:
    name: system:cloud-node-controller
  roleRef:
    apiGroup: rbac.authorization.k8s.io
    kind: ClusterRole
    name: system:cloud-node-controller
  subjects:
  - kind: ServiceAccount
    name: cloud-node-controller
    namespace: kube-system
- apiVersion: rbac.authorization.k8s.io/v1
  kind: ClusterRoleBinding
  metadata:
    name: system:pvl-controller
  roleRef:
    apiGroup: rbac.authorization.k8s.io
    kind: ClusterRole
    name: system:pvl-controller
  subjects:
  - kind: ServiceAccount
    name: pvl-controller
    namespace: kube-system
- apiVersion: rbac.authorization.k8s.io/v1
  kind: ClusterRoleBinding
  metadata:
    name: system:cloud-controller-manager
  roleRef:
    apiGroup: rbac.authorization.k8s.io
    kind: ClusterRole
    name: system:cloud-controller-manager
  subjects:
  - kind: ServiceAccount
    name: cloud-controller-manager
    namespace: kube-system
kind: List
metadata: {}

cloud-controller-manager-roles.yaml

apiVersion: v1
items:
- apiVersion: rbac.authorization.k8s.io/v1
  kind: ClusterRole
  metadata:
    name: system:cloud-controller-manager
  rules:
  - apiGroups:
    - coordination.k8s.io
    resources:
    - leases
    verbs:
    - get
    - create
    - update
  - apiGroups:
    - ""
    resources:
    - events
    verbs:
    - create
    - patch
    - update
  - apiGroups:
    - ""
    resources:
    - nodes
    verbs:
    - '*'
  - apiGroups:
    - ""
    resources:
    - nodes/status
    verbs:
    - patch
  - apiGroups:
    - ""
    resources:
    - services
    verbs:
    - list
    - patch
    - update
    - watch
  - apiGroups:
    - ""
    resources:
    - serviceaccounts
    verbs:
    - create
    - get
  - apiGroups:
    - ""
    resources:
    - serviceaccounts/token
    verbs:
    - create
  - apiGroups:
    - ""
    resources:
    - persistentvolumes
    verbs:
    - '*'
  - apiGroups:
    - ""
    resources:
    - endpoints
    verbs:
    - create
    - get
    - list
    - watch
    - update
  - apiGroups:
    - ""
    resources:
    - configmaps
    verbs:
    - get
    - list
    - watch
  - apiGroups:
    - ""
    resources:
    - secrets
    verbs:
    - list
    - get
    - watch
- apiVersion: rbac.authorization.k8s.io/v1
  kind: ClusterRole
  metadata:
    name: system:cloud-node-controller
  rules:
  - apiGroups:
    - ""
    resources:
    - nodes
    verbs:
    - '*'
  - apiGroups:
    - ""
    resources:
    - nodes/status
    verbs:
    - patch
  - apiGroups:
    - ""
    resources:
    - events
    verbs:
    - create
    - patch
    - update
- apiVersion: rbac.authorization.k8s.io/v1
  kind: ClusterRole
  metadata:
    name: system:pvl-controller
  rules:
  - apiGroups:
    - ""
    resources:
    - persistentvolumes
    verbs:
    - '*'
  - apiGroups:
    - ""
    resources:
    - events
    verbs:
    - create
    - patch
    - update
kind: List
metadata: {}

openstack-cloud-controller-manager-ds.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: cloud-controller-manager
  namespace: kube-system
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: openstack-cloud-controller-manager
  namespace: kube-system
  labels:
    k8s-app: openstack-cloud-controller-manager
spec:
  selector:
    matchLabels:
      k8s-app: openstack-cloud-controller-manager
  updateStrategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        k8s-app: openstack-cloud-controller-manager
    spec:
      nodeSelector:
        node-role.kubernetes.io/controlplane: "true"
      securityContext:
        runAsUser: 1001
      tolerations:
      - key: node.cloudprovider.kubernetes.io/uninitialized
        value: "true"
        effect: NoSchedule
      - key: node-role.kubernetes.io/controlplane
        effect: NoSchedule
        value: "true"
      - key: node-role.kubernetes.io/etcd
        effect: NoExecute
        value: "true"
      serviceAccountName: cloud-controller-manager
      containers:
        - name: openstack-cloud-controller-manager
          image: docker.io/k8scloudprovider/openstack-cloud-controller-manager:latest
          args:
            - /bin/openstack-cloud-controller-manager
            - --v=1
            - --cloud-config=$(CLOUD_CONFIG)
            - --cloud-provider=openstack
            - --use-service-account-credentials=true
            - --bind-address=127.0.0.1
            - --cluster-cidr=10.254.1.0/24
          volumeMounts:
            - mountPath: /etc/kubernetes/pki
              name: k8s-certs
              readOnly: true
            - mountPath: /etc/ssl/certs
              name: ca-certs
              readOnly: true
            - mountPath: /etc/config
              name: cloud-config-volume
              readOnly: true
          resources:
            requests:
              cpu: 200m
          env:
            - name: CLOUD_CONFIG
              value: /etc/config/cloud.conf
      hostNetwork: true
      volumes:
      - hostPath:
          path: /etc/kubernetes/pki
          type: DirectoryOrCreate
        name: k8s-certs
      - hostPath:
          path: /etc/ssl/certs
          type: DirectoryOrCreate
        name: ca-certs
      - name: cloud-config-volume
        secret:
          secretName: cloud-config

Notice that the third file sets the flag –cluster-cidr=10.254.1.0/24. If used in a different ip range, this must be updated.

Apply the three files using kubectl

kubectl apply -f cloud-controller-manager-roles.yaml
kubectl apply -f cloud-controller-manager-role-bindings.yaml
kubectl apply -f openstack-cloud-controller-manager-ds.yaml

This should after a short time update the nodes as shown in Rancher to become initialized and be ready for deployment. This further configures how to deploy load balancer using the GWDG OpenStack deployment and its rather outdated load balancer system.

Cinder CSI Driver

To enable Kubernetes to satisfy PVC using Cinder volumes, the cinder csi plugin must be installed.

Create another yaml file for this.

cinder-csi-plugin.yaml

# This YAML file contains RBAC API objects,
# which are necessary to run csi controller plugin

apiVersion: v1
kind: ServiceAccount
metadata:
  name: csi-cinder-controller-sa
  namespace: kube-system

---
# external attacher
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: csi-attacher-role
rules:
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "patch"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["csinodes"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["volumeattachments"]
    verbs: ["get", "list", "watch", "patch"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["volumeattachments/status"]
    verbs: ["patch"]


---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: csi-attacher-binding
subjects:
  - kind: ServiceAccount
    name: csi-cinder-controller-sa
    namespace: kube-system
roleRef:
  kind: ClusterRole
  name: csi-attacher-role
  apiGroup: rbac.authorization.k8s.io

---
# external Provisioner
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: csi-provisioner-role
rules:
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "update"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["csinodes"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["list", "watch", "create", "update", "patch"]
  - apiGroups: ["snapshot.storage.k8s.io"]
    resources: ["volumesnapshots"]
    verbs: ["get", "list"]
  - apiGroups: ["snapshot.storage.k8s.io"]
    resources: ["volumesnapshotcontents"]
    verbs: ["get", "list"]

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: csi-provisioner-binding
subjects:
  - kind: ServiceAccount
    name: csi-cinder-controller-sa
    namespace: kube-system
roleRef:
  kind: ClusterRole
  name: csi-provisioner-role
  apiGroup: rbac.authorization.k8s.io

---
# external snapshotter
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: csi-snapshotter-role
rules:
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["list", "watch", "create", "update", "patch"]
  # Secret permission is optional.
  # Enable it if your driver needs secret.
  # For example, `csi.storage.k8s.io/snapshotter-secret-name` is set in VolumeSnapshotClass.
  # See https://kubernetes-csi.github.io/docs/secrets-and-credentials.html for more details.
  #  - apiGroups: [""]
  #    resources: ["secrets"]
  #    verbs: ["get", "list"]
  - apiGroups: ["snapshot.storage.k8s.io"]
    resources: ["volumesnapshotclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["snapshot.storage.k8s.io"]
    resources: ["volumesnapshotcontents"]
    verbs: ["create", "get", "list", "watch", "update", "delete"]
  - apiGroups: ["snapshot.storage.k8s.io"]
    resources: ["volumesnapshotcontents/status"]
    verbs: ["update"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: csi-snapshotter-binding
subjects:
  - kind: ServiceAccount
    name: csi-cinder-controller-sa
    namespace: kube-system
roleRef:
  kind: ClusterRole
  name: csi-snapshotter-role
  apiGroup: rbac.authorization.k8s.io
---

# External Resizer
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: csi-resizer-role
rules:
  # The following rule should be uncommented for plugins that require secrets
  # for provisioning.
  # - apiGroups: [""]
  #   resources: ["secrets"]
  #   verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "patch"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims/status"]
    verbs: ["update", "patch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["list", "watch", "create", "update", "patch"]

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: csi-resizer-binding
subjects:
  - kind: ServiceAccount
    name: csi-cinder-controller-sa
    namespace: kube-system
roleRef:
  kind: ClusterRole
  name: csi-resizer-role
  apiGroup: rbac.authorization.k8s.io

---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: kube-system
  name: external-resizer-cfg
rules:
- apiGroups: ["coordination.k8s.io"]
  resources: ["leases"]
  verbs: ["get", "watch", "list", "delete", "update", "create"]

---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: csi-resizer-role-cfg
  namespace: kube-system
subjects:
  - kind: ServiceAccount
    name: csi-cinder-controller-sa
    namespace: kube-system
roleRef:
  kind: Role
  name: external-resizer-cfg
  apiGroup: rbac.authorization.k8s.io

---
# This YAML file contains CSI Controller Plugin Sidecars
# external-attacher, external-provisioner, external-snapshotter
# external-resize, liveness-probe

kind: Service
apiVersion: v1
metadata:
  name: csi-cinder-controller-service
  namespace: kube-system
  labels:
    app: csi-cinder-controllerplugin
spec:
  selector:
    app: csi-cinder-controllerplugin
  ports:
    - name: dummy
      port: 12345

---
kind: StatefulSet
apiVersion: apps/v1
metadata:
  name: csi-cinder-controllerplugin
  namespace: kube-system
spec:
  serviceName: "csi-cinder-controller-service"
  replicas: 1
  selector:
    matchLabels:
      app: csi-cinder-controllerplugin
  template:
    metadata:
      labels:
        app: csi-cinder-controllerplugin
    spec:
      serviceAccount: csi-cinder-controller-sa
      containers:
        - name: csi-attacher
          image: k8s.gcr.io/sig-storage/csi-attacher:v3.1.0
          args:
            - "--csi-address=$(ADDRESS)"
            - "--timeout=3m"
          env:
            - name: ADDRESS
              value: /var/lib/csi/sockets/pluginproxy/csi.sock
          imagePullPolicy: "IfNotPresent"
          volumeMounts:
            - name: socket-dir
              mountPath: /var/lib/csi/sockets/pluginproxy/
        - name: csi-provisioner
          image: k8s.gcr.io/sig-storage/csi-provisioner:v2.1.1
          args:
            - "--csi-address=$(ADDRESS)"
            - "--timeout=3m"
            - "--default-fstype=ext4"
            - "--feature-gates=Topology=true"
            - "--extra-create-metadata"
          env:
            - name: ADDRESS
              value: /var/lib/csi/sockets/pluginproxy/csi.sock
          imagePullPolicy: "IfNotPresent"
          volumeMounts:
            - name: socket-dir
              mountPath: /var/lib/csi/sockets/pluginproxy/
        - name: csi-snapshotter
          image: k8s.gcr.io/sig-storage/csi-snapshotter:v2.1.3
          args:
            - "--csi-address=$(ADDRESS)"
            - "--timeout=3m"
          env:
            - name: ADDRESS
              value: /var/lib/csi/sockets/pluginproxy/csi.sock
          imagePullPolicy: Always
          volumeMounts:
            - mountPath: /var/lib/csi/sockets/pluginproxy/
              name: socket-dir
        - name: csi-resizer
          image: k8s.gcr.io/sig-storage/csi-resizer:v1.1.0
          args:
            - "--csi-address=$(ADDRESS)"
            - "--timeout=3m"
            - "--handle-volume-inuse-error=false"
          env:
            - name: ADDRESS
              value: /var/lib/csi/sockets/pluginproxy/csi.sock
          imagePullPolicy: "IfNotPresent"
          volumeMounts:
            - name: socket-dir
              mountPath: /var/lib/csi/sockets/pluginproxy/
        - name: liveness-probe
          image: k8s.gcr.io/sig-storage/livenessprobe:v2.1.0
          args:
            - "--csi-address=$(ADDRESS)"
          env:
            - name: ADDRESS
              value: /var/lib/csi/sockets/pluginproxy/csi.sock
          volumeMounts:
            - mountPath: /var/lib/csi/sockets/pluginproxy/
              name: socket-dir
        - name: cinder-csi-plugin
          image: docker.io/k8scloudprovider/cinder-csi-plugin:latest
          args:
            - /bin/cinder-csi-plugin
            - "--nodeid=$(NODE_ID)"
            - "--endpoint=$(CSI_ENDPOINT)"
            - "--cloud-config=$(CLOUD_CONFIG)"
            - "--cluster=$(CLUSTER_NAME)"
          env:
            - name: NODE_ID
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: CSI_ENDPOINT
              value: unix://csi/csi.sock
            - name: CLOUD_CONFIG
              value: /etc/config/cloud.conf
            - name: CLUSTER_NAME
              value: kubernetes
          imagePullPolicy: "IfNotPresent"
          ports:
            - containerPort: 9808
              name: healthz
              protocol: TCP
          # The probe
          livenessProbe:
            failureThreshold: 5
            httpGet:
              path: /healthz
              port: healthz
            initialDelaySeconds: 10
            timeoutSeconds: 10
            periodSeconds: 60
          volumeMounts:
            - name: socket-dir
              mountPath: /csi
            - name: secret-cinderplugin
              mountPath: /etc/config
              readOnly: true
      volumes:
        - name: socket-dir
          emptyDir:
        - name: secret-cinderplugin
          secret:
            secretName: cloud-config
---
# This YAML defines all API objects to create RBAC roles for csi node plugin.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: csi-cinder-node-sa
  namespace: kube-system
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: csi-nodeplugin-role
rules:
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: csi-nodeplugin-binding
subjects:
  - kind: ServiceAccount
    name: csi-cinder-node-sa
    namespace: kube-system
roleRef:
  kind: ClusterRole
  name: csi-nodeplugin-role
  apiGroup: rbac.authorization.k8s.io

---
# This YAML file contains driver-registrar & csi driver nodeplugin API objects,
# which are necessary to run csi nodeplugin for cinder.

kind: DaemonSet
apiVersion: apps/v1
metadata:
  name: csi-cinder-nodeplugin
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: csi-cinder-nodeplugin
  template:
    metadata:
      labels:
        app: csi-cinder-nodeplugin
    spec:
      tolerations:
        - operator: Exists
      serviceAccount: csi-cinder-node-sa
      hostNetwork: true
      containers:
        - name: node-driver-registrar
          image: k8s.gcr.io/sig-storage/csi-node-driver-registrar:v1.3.0
          args:
            - "--csi-address=$(ADDRESS)"
            - "--kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)"
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "rm -rf /registration/cinder.csi.openstack.org /registration/cinder.csi.openstack.org-reg.sock"]
          env:
            - name: ADDRESS
              value: /csi/csi.sock
            - name: DRIVER_REG_SOCK_PATH
              value: /var/lib/kubelet/plugins/cinder.csi.openstack.org/csi.sock
            - name: KUBE_NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          imagePullPolicy: "IfNotPresent"
          volumeMounts:
            - name: socket-dir
              mountPath: /csi
            - name: registration-dir
              mountPath: /registration
        - name: liveness-probe
          image: k8s.gcr.io/sig-storage/livenessprobe:v2.1.0
          args:
            - --csi-address=/csi/csi.sock
          volumeMounts:
            - name: socket-dir
              mountPath: /csi
        - name: cinder-csi-plugin
          securityContext:
            privileged: true
            capabilities:
              add: ["SYS_ADMIN"]
            allowPrivilegeEscalation: true
          image: docker.io/k8scloudprovider/cinder-csi-plugin:latest
          args:
            - /bin/cinder-csi-plugin
            - "--nodeid=$(NODE_ID)"
            - "--endpoint=$(CSI_ENDPOINT)"
            - "--cloud-config=$(CLOUD_CONFIG)"
          env:
            - name: NODE_ID
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: CSI_ENDPOINT
              value: unix://csi/csi.sock
            - name: CLOUD_CONFIG
              value: /etc/config/cloud.conf
          imagePullPolicy: "IfNotPresent"
          ports:
            - containerPort: 9808
              name: healthz
              protocol: TCP
          # The probe
          livenessProbe:
            failureThreshold: 5
            httpGet:
              path: /healthz
              port: healthz
            initialDelaySeconds: 10
            timeoutSeconds: 3
            periodSeconds: 10
          volumeMounts:
            - name: socket-dir
              mountPath: /csi
            - name: kubelet-dir
              mountPath: /var/lib/kubelet
              mountPropagation: "Bidirectional"
            - name: pods-probe-dir
              mountPath: /dev
              mountPropagation: "HostToContainer"
            - name: secret-cinderplugin
              mountPath: /etc/config
              readOnly: true
      volumes:
        - name: socket-dir
          hostPath:
            path: /var/lib/kubelet/plugins/cinder.csi.openstack.org
            type: DirectoryOrCreate
        - name: registration-dir
          hostPath:
            path: /var/lib/kubelet/plugins_registry/
            type: Directory
        - name: kubelet-dir
          hostPath:
            path: /var/lib/kubelet
            type: Directory
        - name: pods-probe-dir
          hostPath:
            path: /dev
            type: Directory
        - name: secret-cinderplugin
          secret:
            secretName: cloud-config

---
apiVersion: storage.k8s.io/v1
kind: CSIDriver
metadata:
  name: cinder.csi.openstack.org
spec:
  attachRequired: true
  podInfoOnMount: true
  volumeLifecycleModes:
  - Persistent
  - Ephemeral

---
# This YAML file contains StorageClass definition
# and makes it default storageclass

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: csi-sc-cinderplugin
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: cinder.csi.openstack.org

And apply it using

kubectl apply -f cinder-csi-plugin.yaml

This creates a storage class called csi-sc-cinderplugin that should be used for creating volumes.

Updating cloud.conf

Should it be necessary to update cloud.conf it can be done like this.

Create or update a file called cloud.conf as described above. Replace the existing secret using this:

kubectl create secret generic cloud-config --from-file=cloud.conf --dry-run -n kube-system -o yaml | kubectl apply -n kube-system -f -

Next find the pod running the openstack control manager using

kubectl get pod -n kube-system

and delete it

kubectl delete pod openstack-cloud-controller-manager-xxxx

xxxx will be some random code that needs to be identified via the first command. This will cause the pod to be recreated and to load the new cloud.conf file.

Should any problem occur, the logs of the openstack-cloud-controller-manager pod can also help troubleshoot the issue.

kubectl logs openstack-cloud-controller-manager-xxxx

Validate that it works

The next steps are optional and are just to confirm that the cluster can: - Deploy workloads - Deploy Load balancer that connect to OpenStack - Expose a service - Claim a volume

Deploy a workload

Rancher2.5

Rancher UI -> Global context -> Select your new cluster -> Projects/Namespaces -> Under Project: Default press Add Namespace -> name it ‘test’ -> Create -> Click Project: Default -> Workloads overview should be open -> Deploy -> name it “test-hello” -> as docker image set “rancher/hello-world” -> Launch

Rancher2.6

Rancher UI -> Home -> Select your new cluster -> Projects/Namespaces -> Under Project: Default press Create Namespace -> Name it “test” -> Create -> Workload -> Create -> Deployment -> Namespace to “test” -> Name the workload “test-hello” -> as docker image set “rancher/hello-world” -> Create

Observe that it should be scheduled after a few seconds.

Deploy a load balancer

Rancher2.5

Select Apps -> Launch -> search for Nginx and select NGINX Ingress Controller -> and click it -> namespace to use existing -> test -> Launch

Click on the nginx app and observe that it is deployed and a load balancer is created in the cluster.

Rancher2.6

Select Apps & Marketplace -> search for Nginx and select NGINX Ingress Controller -> click it -> Install -> Set namespace to test -> Next -> change ingress class to “nginx-test” -> Install

After about a minute the load balancer should also appear in OpenStack. Find the load balancer under OpenStack web UI -> Network -> Load Balancers

Via the CLI -> The OpenStack CLI only supports loadbalancers via octavia, the API can still be accessed via manual requests. See [[#Access loadbalancers outside of the web UI]]

If you currently have no free floating IPs, the load balancer setup will not complete. If the load balancer has not claimed a floating IP, release one: OpenStack web UI -> Network -> Floating IPs -> actions -> release floating IP Be careful not to release the floating IP already claimed by the load balancer.

Via the CLI -> Use openstack floating ip list and openstack floating ip delete ID to release one floating IP

Once the load balancer in OpenStack is ready and has claimed an IP, the nginx app in Rancher should also be ready.

Make a note of the floating ip that was claimed.

Expose a service

Rancher2.5

From the nginx App overview -> Resources -> Workloads -> Load Balancing -> Observe that a L4 Balancer already exists -> Add Ingress -> name it “test-ingress” -> Set namespace to “test” -> Set path to “/” -> choose “test-hello” as target -> Set port as 80 -> Open Labels & Annotations drop down -> Add annotation with key “kubernetes.io/ingress.class” and value “nginx” -> Save A hostname ending in sslip.io will be generated.

Rancher2.6

First create a service Service Discovery -> Services -> Create -> ClusterIP -> name it “test-service” -> listening port and target port to 80 -> Selectors -> Set key as “workload.user.cattle.io/workloadselector” and value to “apps.deployment-test-test-hello” -> Create -> Observe that “test-hello” appears as a pod for that service Then create an ingress Service Discovery -> Ingresses -> Create -> name it “test-ingress” -> set request host to “http://test-ingress.test.FLOATING-IP.sslip.io”, where FLOATING-IP is the floating IP noted earlier -> set Path prefix to / -> Select “test-service” as target service and set port to 80 -> Labels&Annotations -> Add Annotation -> Set key to “kubernetes.io/ingress.class” and value “nginx-test” -> Create

The hostname should point to a web page with the Rancher logo writing “Hello World!”. It might take a moment for the link to work. Click the link and confirm that the workload is exposed.

Nginx webhook validation workaround

When trying to create the above described ingress, Rancher might refuse with the error:

Internal error occurred: failed calling webhook “validate.nginx.ingress.kubernetes.io”: an error on the server (“”) has prevented the request from succeeding

A simple workaround for this is to run

kubectl delete -A ValidatingWebhookConfiguration ingress-nginx-admission

This removes the offending validation hook.

Claiming a volume

Rancher2.5

Select Apps -> Launch -> search for mysql and select it -> namespace to use an existing namespace and pick “test” -> activate PVC and ensure it uses the default storage class that is “csi-sc-cinderplugin” -> Launch

Click on the mysql app and observe that it is deployed and a volume claim is created in the cluster.

Rancher2.6

Select Apps&Marketplace -> search for sql and select cockroachdb -> Install -> namespace to “test” -> Next -> Storage per Node to 10Gi -> Install

After a moment the app should deploy and open volume claim(s) that becomes satisfied by volume(s) from OpenStack after a few moments.

In the OpenStack web UI -> Volumes -> Volumes And one volume should have the description “Created by OpenStack Cinder CSI driver”

Or via the CLI -> openstack volume list

Cleanup

Delete the namespace test to cleanup all created resources.

Rancher2.5

Rancher UI -> Global context -> Select cluster -> Projects/Namespaces -> check the box next to the test namepsace -> Delete -> Confirm

Rancher2.6

Rancher UI -> Cluster -> Projects/Namespaces -> check the box next to the test namepsace -> Hold CTRL and press Delete

This also removes the load balancer and volumes in OpenStack. CockroachDB might not remove all its volumes, check manually and remove the remaining volumes.

Appendix

Managing multiple kubeconfigs

Use a tool such as kubectx to manage what context to load. Option 1: One large config Append any new config to the config file. Option 2: Multiple configs Add all files to $KUBECONFIG. You can do so in your shell config by adding

export KUBECONFIG=$KUBECONFIG:$HOME/.kube/config2

Access loadbalancers outside of the web UI

This uses the keystoneauth1 package for authentication and making API requests.

pip install keystoneauth1
import keystoneauth1
# Fill in your credentials
my_username = ""
my_password = ""
my_project_id = ""
# Setting up a session
password_method = keystoneauth1.identity.v3.PasswordMethod(username=my_username,
password=my_password,
user_domain_name="GWDG")
auth = keystoneauth1.identity.v3.Auth(auth_url="https://api.prod.cloud.gwdg.de:5000/v3", auth_methods=[password_method], project_id=my_project_id, project_domain_name="GWDG")
sess = keystoneauth1.session.Session(auth=auth)
# API docs: https://wiki.openstack.org/wiki/Neutron/LBaaS/API_2.0
lbaasv2_api = "https://api.prod.cloud.gwdg.de:9696/v2.0"
# List all Load Balancers
r = sess.request(f"{lbaasv2_api}/lbaas/loadbalancers", "GET",
 headers={"Accept": "application/json"})
print(r.text)