Self-hosted service
yandex-cloud/k8s-csi-s3 avatar
yandex-cloud/k8s-csi-s3

k8s-csi-s3 gives a Kubernetes cluster S3 buckets as PersistentVolumes

GeeseFS-based CSI for mounting S3 buckets as PersistentVolumes

891 stars178 forksGoNOASSERTION

At a glance

What is it?
k8s-csi-s3 is a Container Storage Interface driver that dynamically allocates S3 buckets and mounts them into containers with a FUSE mounter, and it is built by Yandex Cloud with GeeseFS as the recommended backend. It asks for privileged containers and shared mount propagation, it supports three mounters with very different POSIX behaviour, and it is honest that S3 is not a real filesystem.
Who is it for?
Adopt k8s-csi-s3 if you run Kubernetes and want object storage to appear as a PersistentVolume, especially if you already use Yandex Cloud storage or another S3 compatible backend, and if your cluster can run privileged containers. Do not adopt it for a workload that needs real filesystem semantics, since the README is explicit that S3 is not a real file system, that POSIX compatibility varies by mounter, and that the backend may not offer consistency guarantees.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 171 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A CSI driver that allocates buckets and FUSE mounts them

k8s-csi-s3 is a Container Storage Interface driver for S3, or S3 compatible, storage. It can dynamically allocate buckets and mount them via a FUSE mount into any container.

That is the whole mechanism, and it is worth slowing down on, because the design has consequences. A PersistentVolume in Kubernetes is normally backed by a block device or a network filesystem, and applications assume those semantics. Here the volume is an object storage bucket exposed through a FUSE filesystem, which is why the driver exists in the CSI form at all: the CSI is what lets a pod request storage the way it requests any other volume, and the storage class is where you choose the bucket and the mounter.

The audience is a Kubernetes operator who wants object storage mounted into workloads, typically to share large files between pods, to give a job a scratch space backed by a bucket, or to serve static content from the same object store the rest of the platform uses.

The project is from Yandex Cloud, the primary language is Go, and the recent releases are v0.43.7 on 2026-05-05, v0.43.6 on 2026-04-20 and v0.43.5 on 2026-04-20, with the last push on 2026-05-05. The licence field on the repository records a custom licence, and the root of the tree holds both a LICENSE file and an Apache-2.0.txt, which is the text the upstream Kubernetes derived Makefile refers to.

The requirements ask for privileged containers and shared mounts

The requirements section is short, and one of the three lines is the one that decides whether this driver can run in your cluster at all.

Kubernetes 1.17 or newer is the first. The second is that Kubernetes has to allow privileged containers. The third is that the Docker daemon must allow shared mounts, with the systemd flag MountFlags=shared.

Those two constraints are not incidental. A FUSE mount that lives in a container needs elevated privileges to work with the mount namespace, and a mount that has to be propagated into the pods that use it needs the host's mount propagation set to shared so that the mount made in one namespace is visible in another. Without both, the driver can start and the volume will not mount.

This is the honest cost of the approach, and it is worth weighing before you install anything. A driver that requires privileged containers in the cluster has a materially wider blast radius than one that does not, and a cluster configured with a restrictive PodSecurity policy will need an exemption for it.

The requirements are also the reason the mounters differ in behaviour later on, because the GeeseFS default is explicitly not run inside the csi-s3 container, which we will come to.

Helm for the chart, kubectl for the manifests

There are two installation paths, and the README gives both in full.

The Helm chart is published at the Yandex Cloud chart repository, and the two commands are:

bash
helm repo add yandex-s3 https://yandex-cloud.github.io/k8s-csi-s3/charts

helm install csi-s3 yandex-s3/csi-s3

The manual path creates three objects, the provisioner, the driver and the CSI resource, from the deploy/kubernetes directory of the repository:

bash
cd deploy/kubernetes
kubectl create -f provisioner.yaml
kubectl create -f driver.yaml
kubectl create -f csi-s3.yaml

Three files, three roles. The provisioner is what creates buckets when a PersistentVolumeClaim appears, the driver is the component that talks to the nodes and mounts things, and the CSI object is the registration that tells Kubernetes the driver exists and which version of the interface it implements.

The manual path also documents the upgrade migrations, which is more than most driver READMEs do. Upgrading from a version at or below 0.35.5 means deleting all the resources from the old attacher.yaml, and upgrading from a version at or below 0.40.6 means deleting the resources from the old provisioner.yaml, and then reapplying csi-s3.yaml, driver.yaml and provisioner.yaml. Those are not cosmetic steps, because the old and new objects have different names and Kubernetes will not replace them for you.

The last piece of the manual path is the storage class, created from the examples directory, and then a test, which is a PVC, a check that it bound, a pod that mounts it, and a check inside the pod that the mount is really there.

Credentials live in a Secret, and the endpoint decides the backend

The first manual step is a Secret, and the YAML is worth reading carefully because the endpoint line is where the backend gets chosen.

yaml
apiVersion: v1
kind: Secret
metadata:
  name: csi-s3-secret
  # Namespace depends on the configuration in the storageclass.yaml
  namespace: kube-system
stringData:
  accessKeyID: <YOUR_ACCESS_KEY_ID>
  secretAccessKey: <YOUR_SECRET_ACCESS_KEY>
  # For AWS set it to "https://s3.<region>.amazonaws.com", for example https://s3.eu-central-1.amazonaws.com
  endpoint: https://storage.yandexcloud.net
  # For AWS set it to AWS region
  #region: ""

The default endpoint is the Yandex Cloud storage endpoint, and the comments show what to change for AWS: the endpoint becomes the regional S3 URL, and the region key is filled in. The README also says the region can be empty if you are using some other S3 compatible storage, which is the escape hatch for MinIO, Ceph or a similar service.

Two practical notes. The namespace is kube-system by default but the comment points out that it depends on the configuration in the storageclass.yaml, so if the driver runs in a different namespace the Secret has to be created there. And the values are in stringData, which means the credentials are in plain text in the manifest you apply, so that file is something to keep out of version control.

The credential is a single pair of keys for the whole driver, which is worth noting for a multi tenant cluster. There is no per volume credential in this configuration, so the driver authenticates to the bucket as one principal.

A bucket per volume, or a prefix inside a bucket you already have

The bucket behaviour is the part of the design that a storage class controls, and there are two modes.

By default, csi-s3 creates a new bucket per volume, and the bucket name matches the volume ID. If you want your volumes to live in a precreated bucket, you name the bucket in the storage class parameters:

yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: csi-s3-existing-bucket
provisioner: ru.yandex.s3.csi
parameters:
  mounter: geesefs
  options: "--memory-limit 1000 --dir-mode 0777 --file-mode 0666"
  bucket: some-existing-bucket-name

Three things in that manifest are worth reading twice. The provisioner name, ru.yandex.s3.csi, is what ties the storage class to this driver. The mounter parameter selects the FUSE backend, and it defaults to geesefs. And the options string is passed through to the mounter, which is where the directory and file mode flags and the memory limit come from.

And there is a subtlety the README spells out. If the bucket is specified, it will still be created if it does not exist on the backend. Every volume will get its own prefix within the bucket which matches the volume ID, and when deleting a volume, also just the prefix will be deleted.

So naming a bucket in the storage class does not mean the driver will not create it, and it does not mean the driver owns the whole bucket. It means the bucket is shared and the prefixes are owned. The alternative, for mounting a pre-existing bucket or prefix and not having it deleted when the PersistentVolume is deleted, is static provisioning: omit storageClassName in the PersistentVolumeClaim and create a PersistentVolume manually with a matching claimRef, with an example at deploy/kubernetes/examples/pvc-manual.yaml.

Three mounters with very different POSIX behaviour

The mounter choice determines what your workload can actually do with the volume, and the README is unusually candid about the differences.

The default, and the one the project strongly recommends, is GeeseFS. Its listed properties are almost full POSIX compatibility and good performance for both small and big files, with the caveat that it does not store file permissions and custom modification times. There is a second behaviour to know about: by default GeeseFS runs outside of the csi-s3 container using systemd, to not crash mountpoints with a transport endpoint not connected error when csi-s3 is upgraded or restarted. Adding the no-systemd flag to the parameters.options of the StorageClass disables that behaviour.

The two alternatives are s3fs and rclone. s3fs has almost full POSIX compatibility, good performance for big files and poor performance for small files, and is very slow for directories with a large number of files. rclone has poor POSIX compatibility, bad performance for big files and okayish performance for small files, does not create directory objects like s3fs or GeeseFS do, and may hang.

The general point the README makes before that list is that S3 is not a real file system and that there are limitations to consider, with different levels of POSIX compatibility depending on the mounter, and that the storage backend may not always offer consistency guarantees. There is a link to a consistency write-up and to the POSIX compatibility matrix for GeeseFS.

The practical consequence is that a workload which assumes POSIX semantics, with atomic renames and directory listings that are cheap, is a poor fit for any of the three. The mounter is a compatibility decision, not a performance knob.

The image ships three mounters and the tests need a real device

The Dockerfile shows what the runtime image contains, and it is a useful summary of the design.

The build stage compiles the driver as a static binary with CGO disabled and the external linker flags for static linking, and the runtime stage is Alpine with the fuse package, mailcap and rclone installed, plus s3fs-fuse from the Alpine community repository, and then the GeeseFS binary is downloaded from its GitHub releases and dropped into /usr/bin. The entrypoint is the driver itself.

So a single image carries all three mounters, and the choice of which one runs is made per storage class rather than per image. That is a sensible packaging decision for a driver that supports three backends, and it also means the image pulls GeeseFS from a GitHub release at build time, which is worth knowing if you build the image in an environment with restricted egress.

The Makefile in the repository shows the release and test flow. The build target compiles the driver binary with the same static flags, the container target builds the image, and the push target tags it into two registries, the cr.yandex and cr.il.nebius.cloud ones, and the test target is the interesting one:

makefile
test:
	docker build -t $(TEST_IMAGE_TAG) -f test/Dockerfile .
	docker run --rm --privileged -v $(PWD):/build --device /dev/fuse $(TEST_IMAGE_TAG)

The test container runs privileged and passes the host's fuse device through, because the driver under test has to mount things for real. The README says the driver is tested by the CSI Sanity Tester from the Kubernetes CSI test project, and that as end to end tests require S3 storage and a mounter like s3fs, this is best done in a docker container, with a Dockerfile and the test script in the test directory.

Editorial conclusion

Adopt k8s-csi-s3 if you run Kubernetes and want object storage to appear as a PersistentVolume, especially if you already use Yandex Cloud storage or another S3 compatible backend, and if your cluster can run privileged containers. Do not adopt it for a workload that needs real filesystem semantics, since the README is explicit that S3 is not a real file system, that POSIX compatibility varies by mounter, and that the backend may not offer consistency guarantees. Before you deploy it, read the permissions requirements in full, decide between GeeseFS, s3fs and rclone from the compatibility notes rather than from habit, and if you use an existing bucket, understand that the driver still creates it if it is missing and that each volume maps to a prefix inside it.

Frequently asked questions

What does the k8s-csi-s3 driver do?

It is a Container Storage Interface driver for S3 and S3 compatible storage that dynamically allocates buckets and mounts them via a FUSE mount into any container, so a bucket can be used as a Kubernetes PersistentVolume.

What are the requirements for running k8s-csi-s3?

Kubernetes 1.17 or newer, Kubernetes has to allow privileged containers, and the Docker daemon must allow shared mounts with the systemd flag MountFlags=shared. The Helm chart is published at the yandex-cloud chart repository.

Which mounter should I use with k8s-csi-s3?

The README strongly recommends the default, GeeseFS, which has almost full POSIX compatibility and good performance for small and big files but does not store file permissions or custom modification times. s3fs and rclone are supported as alternatives with weaker performance or compatibility.

What happens when I name an existing bucket in the storage class?

The bucket will still be created if it does not exist on the backend, every volume gets its own prefix inside it matching the volume ID, and deleting a volume deletes only that prefix. Static provisioning is the way to mount a pre-existing bucket or prefix without the driver deleting it.

How do I check whether the k8s-csi-s3 mount is working?

Create a PVC and a pod from the examples, then inside the pod run mount and grep for fuse, which should show the volume mounted as type fuse.geesefs, and create a file in it. For problems, the README points at the provisioner and driver logs with kubectl logs.

Official sources

  1. Issues
  2. README
  3. Releases
  4. yandex-cloud/k8s-csi-s3 on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/yandex-cloud-k8s-csi-s3.svg)](https://hysenlabs.com/projects/yandex-cloud-k8s-csi-s3)

Was this useful?