diff --git a/contrib/terraform/upcloud/modules/kubernetes-cluster/main.tf b/contrib/terraform/upcloud/modules/kubernetes-cluster/main.tf
index c72788c69753c5cb00178daecee8b09e19dfcaf6..bce7c924abf1e1fae9e937cb0bfa252f37dc59c9 100644
--- a/contrib/terraform/upcloud/modules/kubernetes-cluster/main.tf
+++ b/contrib/terraform/upcloud/modules/kubernetes-cluster/main.tf
@@ -65,6 +65,11 @@ resource "upcloud_server" "master" {
     network = upcloud_network.private.id
   }
 
+  # Ignore volumes created by csi-driver
+  lifecycle {
+    ignore_changes = [storage_devices]
+  }
+
   dynamic "storage_devices" {
     for_each = {
       for disk_key_name, disk in upcloud_storage.additional_disks :
@@ -114,6 +119,11 @@ resource "upcloud_server" "worker" {
     network = upcloud_network.private.id
   }
 
+  # Ignore volumes created by csi-driver
+  lifecycle {
+    ignore_changes = [storage_devices]
+  }
+
   dynamic "storage_devices" {
     for_each = {
       for disk_key_name, disk in upcloud_storage.additional_disks :
diff --git a/inventory/sample/group_vars/all/upcloud.yml b/inventory/sample/group_vars/all/upcloud.yml
new file mode 100644
index 0000000000000000000000000000000000000000..060f632498c9ec45e4dda19179045a1524f4d5f2
--- /dev/null
+++ b/inventory/sample/group_vars/all/upcloud.yml
@@ -0,0 +1,17 @@
+## Repo for UpClouds csi-driver: https://github.com/UpCloudLtd/upcloud-csi
+## To use UpClouds CSI plugin to provision volumes set this value to true
+## Remember to set UPCLOUD_USERNAME and UPCLOUD_PASSWORD
+# upcloud_csi_enabled: true
+# upcloud_csi_controller_replicas: 1
+## Override used image tags
+# upcloud_csi_provisioner_image_tag: "v3.1.0"
+# upcloud_csi_attacher_image_tag: "v3.4.0"
+# upcloud_csi_resizer_image_tag: "v1.4.0"
+# upcloud_csi_plugin_image_tag: "alpha"
+# upcloud_csi_node_image_tag: "v2.5.0"
+# upcloud_tolerations: []
+## Storage class options
+# expand_persistent_volumes: true
+# storage_classes:
+#   - name: standard
+#     is_default: true
diff --git a/inventory/sample/group_vars/k8s_cluster/k8s-cluster.yml b/inventory/sample/group_vars/k8s_cluster/k8s-cluster.yml
index 5749c08ba78f234b41e0a5648fa2fc1a2176697f..fd8b5cd0d12633bc229952220ba7a874fd942b78 100644
--- a/inventory/sample/group_vars/k8s_cluster/k8s-cluster.yml
+++ b/inventory/sample/group_vars/k8s_cluster/k8s-cluster.yml
@@ -238,6 +238,8 @@ podsecuritypolicy_enabled: false
 
 # Make a copy of kubeconfig on the host that runs Ansible in {{ inventory_dir }}/artifacts
 # kubeconfig_localhost: false
+# Use ansible_host as external api ip when copying over kubeconfig.
+# kubeconfig_localhost_ansible_host: false
 # Download kubectl onto the host that runs Ansible in {{ bin_dir }}
 # kubectl_localhost: false
 
diff --git a/roles/kubernetes-apps/csi_driver/upcloud/defaults/main.yml b/roles/kubernetes-apps/csi_driver/upcloud/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..810d00fd24043dcb1e21693fffab3ae41de0793f
--- /dev/null
+++ b/roles/kubernetes-apps/csi_driver/upcloud/defaults/main.yml
@@ -0,0 +1,10 @@
+---
+upcloud_csi_controller_replicas: 1
+upcloud_csi_provisioner_image_tag: "v3.1.0"
+upcloud_csi_attacher_image_tag: "v3.4.0"
+upcloud_csi_resizer_image_tag: "v1.4.0"
+upcloud_csi_plugin_image_tag: "alpha"
+upcloud_csi_node_image_tag: "v2.5.0"
+upcloud_username: "{{ lookup('env','UPCLOUD_USERNAME')  }}"
+upcloud_password: "{{ lookup('env','UPCLOUD_PASSWORD')  }}"
+upcloud_tolerations: []
\ No newline at end of file
diff --git a/roles/kubernetes-apps/csi_driver/upcloud/tasks/main.yml b/roles/kubernetes-apps/csi_driver/upcloud/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e11e8e2d15077d4ecf1a5b88005dbe94346ab93f
--- /dev/null
+++ b/roles/kubernetes-apps/csi_driver/upcloud/tasks/main.yml
@@ -0,0 +1,43 @@
+---
+- name: UpCloud CSI Driver | Check if UPCLOUD_USERNAME exists
+  fail:
+    msg: "UpCloud username is missing. Env UPCLOUD_USERNAME is mandatory"
+  when: upcloud_username is not defined or not upcloud_username
+  tags: upcloud-csi-driver
+
+- name: UpCloud CSI Driver | Check if UPCLOUD_PASSWORD exists
+  fail:
+    msg: "UpCloud password is missing. Env UPCLOUD_PASSWORD is mandatory"
+  when:
+    - upcloud_username is defined
+    - upcloud_username|length > 0
+    - upcloud_password is not defined or not upcloud_password
+  tags: upcloud-csi-driver
+
+- name: UpCloud CSI Driver | Generate Manifests
+  template:
+    src: "{{ item.file }}.j2"
+    dest: "{{ kube_config_dir }}/{{ item.file }}"
+  with_items:
+    - {name: upcloud-csi-cred-secret, file: upcloud-csi-cred-secret.yml}
+    - {name: upcloud-csi-setup, file: upcloud-csi-setup.yml}
+    - {name: upcloud-csi-controller, file: upcloud-csi-controller.yml}
+    - {name: upcloud-csi-node, file: upcloud-csi-node.yml}
+    - {name: upcloud-csi-driver, file: upcloud-csi-driver.yml}
+  register: upcloud_csi_manifests
+  when: inventory_hostname == groups['kube_control_plane'][0]
+  tags: upcloud-csi-driver
+
+- name: UpCloud CSI Driver | Apply Manifests
+  kube:
+    kubectl: "{{ bin_dir }}/kubectl"
+    filename: "{{ kube_config_dir }}/{{ item.item.file }}"
+    state: "latest"
+  with_items:
+    - "{{ upcloud_csi_manifests.results }}"
+  when:
+    - inventory_hostname == groups['kube_control_plane'][0]
+    - not item is skipped
+  loop_control:
+    label: "{{ item.item.file }}"
+  tags: upcloud-csi-driver
diff --git a/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-controller.yml.j2 b/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-controller.yml.j2
new file mode 100644
index 0000000000000000000000000000000000000000..71228aabffdc97812c30c1986476fed971a12a88
--- /dev/null
+++ b/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-controller.yml.j2
@@ -0,0 +1,96 @@
+kind: StatefulSet
+apiVersion: apps/v1
+metadata:
+  name: csi-upcloud-controller
+  namespace: kube-system
+spec:
+  serviceName: "csi-upcloud"
+  replicas: {{ upcloud_csi_controller_replicas }}
+  selector:
+    matchLabels:
+      app: csi-upcloud-controller
+  template:
+    metadata:
+      labels:
+        app: csi-upcloud-controller
+        role: csi-upcloud
+    spec:
+      priorityClassName: system-cluster-critical
+      serviceAccount: csi-upcloud-controller-sa
+      containers:
+        - name: csi-provisioner
+          image: k8s.gcr.io/sig-storage/csi-provisioner:{{ upcloud_csi_provisioner_image_tag }}
+          args:
+            - "--csi-address=$(ADDRESS)"
+            - "--v=5"
+            - "--timeout=60s"
+          env:
+            - name: ADDRESS
+              value: /var/lib/csi/sockets/pluginproxy/csi.sock
+          imagePullPolicy: "Always"
+          volumeMounts:
+            - name: socket-dir
+              mountPath: /var/lib/csi/sockets/pluginproxy/
+        - name: csi-attacher
+          image: k8s.gcr.io/sig-storage/csi-attacher:{{ upcloud_csi_attacher_image_tag }}
+          args:
+            - "--v=5"
+            - "--csi-address=$(ADDRESS)"
+            - "--timeout=30s"
+          env:
+            - name: ADDRESS
+              value: /var/lib/csi/sockets/pluginproxy/csi.sock
+          imagePullPolicy: "Always"
+          volumeMounts:
+            - name: socket-dir
+              mountPath: /var/lib/csi/sockets/pluginproxy/
+        - name: csi-resizer
+          image: k8s.gcr.io/sig-storage/csi-resizer:{{ upcloud_csi_resizer_image_tag }}
+          args:
+            - "--v=5"
+            - "--timeout=45s"
+            - "--csi-address=$(ADDRESS)"
+            - "--handle-volume-inuse-error=true"
+          env:
+            - name: ADDRESS
+              value: /var/lib/csi/sockets/pluginproxy/csi.sock
+          imagePullPolicy: "Always"
+          volumeMounts:
+            - name: socket-dir
+              mountPath: /var/lib/csi/sockets/pluginproxy/
+        - name: csi-upcloud-plugin
+          image: ghcr.io/upcloudltd/upcloud-csi:{{ upcloud_csi_plugin_image_tag }}
+          args:
+            - "--endpoint=$(CSI_ENDPOINT)"
+            - "--nodehost=$(NODE_ID)"
+            - "--username=$(UPCLOUD_USERNAME)"
+            - "--password=$(UPCLOUD_PASSWORD)"
+            - "--url=$(UPCLOUD_API_URL)"
+          env:
+            - name: CSI_ENDPOINT
+              value: unix:///var/lib/csi/sockets/pluginproxy/csi.sock
+            - name: UPCLOUD_API_URL
+              value: https://api.upcloud.com/
+            - name: UPCLOUD_USERNAME
+              valueFrom:
+                secretKeyRef:
+                  name: upcloud
+                  key: username
+            - name: UPCLOUD_PASSWORD
+              valueFrom:
+                secretKeyRef:
+                  name: upcloud
+                  key: password
+            - name: NODE_ID
+              valueFrom:
+                fieldRef:
+                  fieldPath: spec.nodeName
+          imagePullPolicy: "Always"
+          volumeMounts:
+            - name: socket-dir
+              mountPath: /var/lib/csi/sockets/pluginproxy/
+      imagePullSecrets:
+        - name: regcred
+      volumes:
+        - name: socket-dir
+          emptyDir: { }
\ No newline at end of file
diff --git a/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-cred-secret.yml.j2 b/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-cred-secret.yml.j2
new file mode 100644
index 0000000000000000000000000000000000000000..5e91d884d40159f7d573cb9adb68afd012b6fd66
--- /dev/null
+++ b/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-cred-secret.yml.j2
@@ -0,0 +1,9 @@
+---
+apiVersion: v1
+kind: Secret
+metadata:
+    name: upcloud
+    namespace: kube-system
+stringData:
+    username: {{ upcloud_username }}
+    password: {{ upcloud_password }}
diff --git a/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-driver.yml.j2 b/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-driver.yml.j2
new file mode 100644
index 0000000000000000000000000000000000000000..b5c45532489f7dbbd0df2b8acc9d4a0ee3a76ce5
--- /dev/null
+++ b/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-driver.yml.j2
@@ -0,0 +1,7 @@
+apiVersion: storage.k8s.io/v1
+kind: CSIDriver
+metadata:
+  name: storage.csi.upcloud.com
+spec:
+  attachRequired: true
+  podInfoOnMount: true
\ No newline at end of file
diff --git a/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-node.yml.j2 b/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-node.yml.j2
new file mode 100644
index 0000000000000000000000000000000000000000..6961b8ff13c5469d641170610744f2870c58aea1
--- /dev/null
+++ b/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-node.yml.j2
@@ -0,0 +1,113 @@
+kind: DaemonSet
+apiVersion: apps/v1
+metadata:
+  name: csi-upcloud-node
+  namespace: kube-system
+spec:
+  selector:
+    matchLabels:
+      app: csi-upcloud-node
+  template:
+    metadata:
+      labels:
+        app: csi-upcloud-node
+        role: csi-upcloud
+    spec:
+      priorityClassName: system-node-critical
+      serviceAccount: csi-upcloud-node-sa
+      hostNetwork: true
+      containers:
+        - name: csi-node-driver-registrar
+          image: k8s.gcr.io/sig-storage/csi-node-driver-registrar:{{ upcloud_csi_node_image_tag }}
+          args:
+            - "--v=5"
+            - "--csi-address=$(ADDRESS)"
+            - "--kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)"
+          lifecycle:
+            preStop:
+              exec:
+                command:
+                  [
+                      "/bin/sh",
+                      "-c",
+                      "rm -rf /registration/storage.csi.upcloud.com /registration/storage.csi.upcloud.com-reg.sock",
+                  ]
+          env:
+            - name: ADDRESS
+              value: /csi/csi.sock
+            - name: DRIVER_REG_SOCK_PATH
+              value: /var/lib/kubelet/plugins/storage.csi.upcloud.com/csi.sock
+            - name: KUBE_NODE_NAME
+              valueFrom:
+                fieldRef:
+                  fieldPath: spec.nodeName
+          volumeMounts:
+            - name: plugin-dir
+              mountPath: /csi/
+            - name: registration-dir
+              mountPath: /registration/
+        - name: csi-upcloud-plugin
+          image: ghcr.io/upcloudltd/upcloud-csi:alpha
+          args:
+            - "--endpoint=$(CSI_ENDPOINT)"
+            - "--nodehost=$(NODE_ID)"
+            - "--username=$(UPCLOUD_USERNAME)"
+            - "--password=$(UPCLOUD_PASSWORD)"
+            - "--url=$(UPCLOUD_API_URL)"
+          env:
+            - name: CSI_ENDPOINT
+              value: unix:///csi/csi.sock
+            - name: UPCLOUD_API_URL
+              value: https://api.upcloud.com/
+            - name: UPCLOUD_USERNAME
+              valueFrom:
+                secretKeyRef:
+                  name: upcloud
+                  key: username
+            - name: UPCLOUD_PASSWORD
+              valueFrom:
+                secretKeyRef:
+                  name: upcloud
+                  key: password
+            - name: NODE_ID
+              valueFrom:
+                fieldRef:
+                  fieldPath: spec.nodeName
+          imagePullPolicy: "Always"
+          securityContext:
+            privileged: true
+            capabilities:
+              add: [ "SYS_ADMIN" ]
+            allowPrivilegeEscalation: true
+          volumeMounts:
+            - name: plugin-dir
+              mountPath: /csi
+            - name: pods-mount-dir
+              mountPath: /var/lib/kubelet
+              # needed so that any mounts setup inside this container are
+              # propagated back to the host machine.
+              mountPropagation: "Bidirectional"
+            - name: device-dir
+              mountPath: /dev
+      imagePullSecrets:
+        - name: regcred
+      volumes:
+        - name: registration-dir
+          hostPath:
+            path: /var/lib/kubelet/plugins_registry/
+            type: DirectoryOrCreate
+        - name: plugin-dir
+          hostPath:
+            path: /var/lib/kubelet/plugins/storage.csi.upcloud.com
+            type: DirectoryOrCreate
+        - name: pods-mount-dir
+          hostPath:
+            path: /var/lib/kubelet
+            type: Directory
+        - name: device-dir
+          hostPath:
+            path: /dev
+{% if upcloud_tolerations %}
+      tolerations:
+        {{ upcloud_tolerations | to_nice_yaml(indent=2) | indent(width=8) }}
+{% endif %}
\ No newline at end of file
diff --git a/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-setup.yml.j2 b/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-setup.yml.j2
new file mode 100644
index 0000000000000000000000000000000000000000..2a9ec08beeae1320c28ec30ada8579560d02d45a
--- /dev/null
+++ b/roles/kubernetes-apps/csi_driver/upcloud/templates/upcloud-csi-setup.yml.j2
@@ -0,0 +1,205 @@
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: csi-upcloud-controller-sa
+  namespace: kube-system
+
+---
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: csi-upcloud-provisioner-role
+rules:
+  - apiGroups: [ "" ]
+    resources: [ "secrets" ]
+    verbs: [ "get", "list" ]
+  - 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: [ "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" ]
+  - apiGroups: [ "" ]
+    resources: [ "nodes" ]
+    verbs: [ "get", "list", "watch" ]
+
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: csi-upcloud-provisioner-binding
+subjects:
+  - kind: ServiceAccount
+    name: csi-upcloud-controller-sa
+    namespace: kube-system
+roleRef:
+  kind: ClusterRole
+  name: csi-upcloud-provisioner-role
+  apiGroup: rbac.authorization.k8s.io
+
+---
+# Attacher must be able to work with PVs, nodes and VolumeAttachments
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: csi-upcloud-attacher-role
+rules:
+  - apiGroups: [ "" ]
+    resources: [ "persistentvolumes" ]
+    verbs: [ "get", "list", "watch", "update", "patch" ]
+  - apiGroups: [ "" ]
+    resources: [ "nodes" ]
+    verbs: [ "get", "list", "watch" ]
+  - apiGroups: [ "storage.k8s.io" ]
+    resources: [ "csinodes" ]
+    verbs: [ "get", "list", "watch" ]
+  - apiGroups: [ "storage.k8s.io" ]
+    resources: [ "volumeattachments" ]
+    verbs: [ "get", "list", "watch", "update", "patch" ]
+  - apiGroups: [ "storage.k8s.io" ]
+    resources: [ "volumeattachments/status" ]
+    verbs: [ "get", "list", "watch", "update", "patch" ]
+
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: csi-upcloud-attacher-binding
+subjects:
+  - kind: ServiceAccount
+    name: csi-upcloud-controller-sa
+    namespace: kube-system
+roleRef:
+  kind: ClusterRole
+  name: csi-upcloud-attacher-role
+  apiGroup: rbac.authorization.k8s.io
+
+---
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: csi-upcloud-snapshotter-role
+rules:
+  - apiGroups: [ "" ]
+    resources: [ "persistentvolumes" ]
+    verbs: [ "get", "list", "watch" ]
+  - apiGroups: [ "" ]
+    resources: [ "persistentvolumeclaims" ]
+    verbs: [ "get", "list", "watch" ]
+  - apiGroups: [ "storage.k8s.io" ]
+    resources: [ "storageclasses" ]
+    verbs: [ "get", "list", "watch" ]
+  - apiGroups: [ "" ]
+    resources: [ "events" ]
+    verbs: [ "list", "watch", "create", "update", "patch" ]
+  - 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: [ "volumesnapshots" ]
+    verbs: [ "get", "list", "watch", "update" ]
+  - apiGroups: [ "apiextensions.k8s.io" ]
+    resources: [ "customresourcedefinitions" ]
+    verbs: [ "create", "list", "watch", "delete" ]
+
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: csi-upcloud-snapshotter-binding
+subjects:
+  - kind: ServiceAccount
+    name: csi-upcloud-controller-sa
+    namespace: kube-system
+roleRef:
+  kind: ClusterRole
+  name: csi-upcloud-snapshotter-role
+  apiGroup: rbac.authorization.k8s.io
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: csi-upcloud-node-sa
+  namespace: kube-system
+
+---
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: csi-upcloud-node-driver-registrar-role
+  namespace: kube-system
+rules:
+  - apiGroups: [ "" ]
+    resources: [ "events" ]
+    verbs: [ "get", "list", "watch", "create", "update", "patch" ]
+
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: csi-upcloud-node-driver-registrar-binding
+subjects:
+  - kind: ServiceAccount
+    name: csi-upcloud-node-sa
+    namespace: kube-system
+roleRef:
+  kind: ClusterRole
+  name: csi-upcloud-node-driver-registrar-role
+  apiGroup: rbac.authorization.k8s.io
+---
+# Resizer must be able to work with PVCs, PVs, SCs.
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: csi-upcloud-resizer-role
+rules:
+  - apiGroups: [ "" ]
+    resources: [ "persistentvolumes" ]
+    verbs: [ "get", "list", "watch", "update", "patch" ]
+  - apiGroups: [ "" ]
+    resources: [ "persistentvolumeclaims" ]
+    verbs: [ "get", "list", "watch" ]
+  - apiGroups: [ "" ]
+    resources: [ "persistentvolumeclaims/status" ]
+    verbs: [ "update", "patch" ]
+  - apiGroups: [ "" ]
+    resources: [ "events" ]
+    verbs: [ "list", "watch", "create", "update", "patch" ]
+  - apiGroups: [ "" ]
+    resources: [ "pods" ]
+    verbs: [ "watch", "list" ]
+
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: csi-upcloud-resizer-binding
+subjects:
+  - kind: ServiceAccount
+    name: csi-upcloud-controller-sa
+    namespace: kube-system
+roleRef:
+  kind: ClusterRole
+  name: csi-upcloud-resizer-role
+  apiGroup: rbac.authorization.k8s.io
\ No newline at end of file
diff --git a/roles/kubernetes-apps/meta/main.yml b/roles/kubernetes-apps/meta/main.yml
index 4650b38c162c0d034e87af527a266c3500bcd150..9c19fdebd76f5f9433f11cb5959f6e3334b8e39a 100644
--- a/roles/kubernetes-apps/meta/main.yml
+++ b/roles/kubernetes-apps/meta/main.yml
@@ -65,6 +65,13 @@ dependencies:
       - gcp-pd-csi-driver
       - csi-driver
 
+  - role: kubernetes-apps/csi_driver/upcloud
+    when:
+      - upcloud_csi_enabled
+    tags:
+      - upcloud-csi-driver
+      - csi-driver
+
   - role: kubernetes-apps/csi_driver/vsphere
     when:
       - vsphere_csi_enabled
diff --git a/roles/kubernetes-apps/persistent_volumes/meta/main.yml b/roles/kubernetes-apps/persistent_volumes/meta/main.yml
index c0522df9c9bb962677f3a43dc22be88692443e17..fdfd80778bdd6d5f180807e364e56f635e78ff6a 100644
--- a/roles/kubernetes-apps/persistent_volumes/meta/main.yml
+++ b/roles/kubernetes-apps/persistent_volumes/meta/main.yml
@@ -34,3 +34,10 @@ dependencies:
     tags:
       - persistent_volumes_gcp_pd_csi
       - gcp-pd-csi-driver
+
+  - role: kubernetes-apps/persistent_volumes/upcloud-csi
+    when:
+      - upcloud_csi_enabled
+    tags:
+      - persistent_volumes_upcloud_csi
+      - upcloud-csi-driver
\ No newline at end of file
diff --git a/roles/kubernetes-apps/persistent_volumes/upcloud-csi/defaults/main.yml b/roles/kubernetes-apps/persistent_volumes/upcloud-csi/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2e4726c1fc11d37bd8f09b62cce17bacc2cd5294
--- /dev/null
+++ b/roles/kubernetes-apps/persistent_volumes/upcloud-csi/defaults/main.yml
@@ -0,0 +1,5 @@
+---
+expand_persistent_volumes: true
+storage_classes:
+  - name: standard
+    is_default: true
diff --git a/roles/kubernetes-apps/persistent_volumes/upcloud-csi/tasks/main.yml b/roles/kubernetes-apps/persistent_volumes/upcloud-csi/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f63e49a9b3d72db8d6c9368ddc859b632d7c8b04
--- /dev/null
+++ b/roles/kubernetes-apps/persistent_volumes/upcloud-csi/tasks/main.yml
@@ -0,0 +1,19 @@
+---
+- name: Kubernetes Persistent Volumes | Copy UpCloud CSI Storage Class template
+  template:
+    src: "upcloud-csi-storage-class.yml.j2"
+    dest: "{{ kube_config_dir }}/upcloud-csi-storage-class.yml"
+  register: manifests
+  when:
+    - inventory_hostname == groups['kube_control_plane'][0]
+
+- name: Kubernetes Persistent Volumes | Add UpCloud CSI Storage Class
+  kube:
+    name: upcloud-csi
+    kubectl: "{{ bin_dir }}/kubectl"
+    resource: StorageClass
+    filename: "{{ kube_config_dir }}/upcloud-csi-storage-class.yml"
+    state: "latest"
+  when:
+    - inventory_hostname == groups['kube_control_plane'][0]
+    - manifests.changed
diff --git a/roles/kubernetes-apps/persistent_volumes/upcloud-csi/templates/upcloud-csi-storage-class.yml.j2 b/roles/kubernetes-apps/persistent_volumes/upcloud-csi/templates/upcloud-csi-storage-class.yml.j2
new file mode 100644
index 0000000000000000000000000000000000000000..fe66801fa5c858ced633408a9be0fa4196e4c50e
--- /dev/null
+++ b/roles/kubernetes-apps/persistent_volumes/upcloud-csi/templates/upcloud-csi-storage-class.yml.j2
@@ -0,0 +1,11 @@
+{% for class in storage_classes %}
+---
+kind: StorageClass
+apiVersion: storage.k8s.io/v1
+metadata:
+  name: "{{ class.name }}"
+  annotations:
+    storageclass.kubernetes.io/is-default-class: "{{ class.is_default | default(false) | ternary("true","false") }}"
+provisioner: storage.csi.upcloud.com
+allowVolumeExpansion: {{ expand_persistent_volumes }}
+{% endfor %}
diff --git a/roles/kubernetes/client/defaults/main.yml b/roles/kubernetes/client/defaults/main.yml
index 32870df016a6ac1baf2066950ffca852f8bf4566..83506a41a002812b8ccc27c2a9409e384c4ada57 100644
--- a/roles/kubernetes/client/defaults/main.yml
+++ b/roles/kubernetes/client/defaults/main.yml
@@ -1,5 +1,6 @@
 ---
 kubeconfig_localhost: false
+kubeconfig_localhost_ansible_host: false
 kubectl_localhost: false
 artifacts_dir: "{{ inventory_dir }}/artifacts"
 
diff --git a/roles/kubernetes/client/tasks/main.yml b/roles/kubernetes/client/tasks/main.yml
index bde0f006e4214a856dcce0da0f5ed6b5aa3e38ee..9621e2f541fc876712de68ed201d112b8211265a 100644
--- a/roles/kubernetes/client/tasks/main.yml
+++ b/roles/kubernetes/client/tasks/main.yml
@@ -4,6 +4,8 @@
     external_apiserver_address: >-
       {%- if loadbalancer_apiserver is defined and loadbalancer_apiserver.address is defined -%}
       {{ loadbalancer_apiserver.address }}
+      {%- elif kubeconfig_localhost_ansible_host is defined and kubeconfig_localhost_ansible_host -%}
+      {{ hostvars[groups['kube_control_plane'][0]].ansible_host }}
       {%- else -%}
       {{ kube_apiserver_access_address }}
       {%- endif -%}
diff --git a/roles/kubespray-defaults/defaults/main.yaml b/roles/kubespray-defaults/defaults/main.yaml
index 8615f60461ea07d0a88b0568f87f0c10b72b6984..351b4d60e1af643aeb0ded270ca103d30f227022 100644
--- a/roles/kubespray-defaults/defaults/main.yaml
+++ b/roles/kubespray-defaults/defaults/main.yaml
@@ -403,6 +403,7 @@ aws_ebs_csi_enabled: false
 azure_csi_enabled: false
 gcp_pd_csi_enabled: false
 vsphere_csi_enabled: false
+upcloud_csi_enabled: false
 csi_snapshot_controller_enabled: false
 persistent_volumes_enabled: false
 cephfs_provisioner_enabled: false