apiVersion: v1
kind: Service
metadata:
  name: mongodb
  labels:
    app: mongodb
spec:
  clusterIP: None
  selector:
    app: mongodb
  ports:
    - port: 27017
      targetPort: 27017

Save the above YAML in mongodb-svc.yaml and create the Service by running kubectl apply -f mongodb-svc.yaml. If you list the Services, you will notice the mongodb Service does not have an IP address set:

$ kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)     AGE
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP     47h
mongodb      ClusterIP   None         <none>        27017/TCP   1s

Next, we will create the following StatefulSet:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb
spec:
  serviceName: mongodb
  replicas: 1
  selector:
    matchLabels:
      app: mongodb
  template:
    metadata:
      labels:
        app: mongodb
        selector: mongodb
    spec:
      containers:
        - name: mongodb
          image: mongo:4.0.17
          ports:
            - containerPort: 27017
          volumeMounts:
            - name: pvc
              mountPath: /data/db
  volumeClaimTemplates:
    - metadata:
        name: pvc
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 1Gi

Save the above YAML in mongodb-statefulset.yaml and create the StatefulSet by running kubectl apply -f mongodb-statefulset.yaml.

If you list the Pods, you will notice you created a single Pod named mongodb-0:

$ kubectl get pods
NAME        READY   STATUS    RESTARTS   AGE
mongodb-0   1/1     Running   0          29m

Similarly, let's list the PersistentVolumes and PersistentVolumeClaims:

$ kubectl get pv,pvc
NAME                                                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                   STORAGECLASS   REASON   AGE
persistentvolume/pvc-275f7731-0f10-4b33-b99a-3cc95d0be30a   1Gi        RWO            Delete           Bound    default/pvc-mongodb-0   standard                31m

NAME                                  STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/pvc-mongodb-0   Bound    pvc-275f7731-0f10-4b33-b99a-3cc95d0be30a   1Gi        RWO            standard       31m

Notice the naming of both resources - the PVC uses the name (pvc) we provided under the volumeClaimTemplates, and it appends the name of the Pod (mongodb-0) to it. Kubernetes prefixes the PersistentVolume name with the PVC name (pvc). The rest of the name is a unique identifier.

Let's see what happens if we scale the StatefulSet to 3 Pods:

$ kubectl scale statefulset mongodb --replicas=3
statefulset.apps/mongodb scaled

If you watch the Pods as Kubernetes creates them (kubectl get pods -w) you will notice they are created in order - the replica named mongodb-1 is created first, and after that replica mongodb-2 is created.

A StatefulSet stores the name of the pod in label called statefulset.kubernetes.io/pod-name. You can see these labels if you run kubectl get po --show-labels:

$ kubectl get po --show-labels
NAME        READY   STATUS    RESTARTS   AGE     LABELS
mongodb-0   1/1     Running   0          44m     statefulset.kubernetes.io/pod-name=mongodb-0
mongodb-1   1/1     Running   0          2m34s   statefulset.kubernetes.io/pod-name=mongodb-1
mongodb-2   1/1     Running   0          2m30s   statefulset.kubernetes.io/pod-name=mongodb-2