Running Cluster on Kubernetes

This guide will show you how to:

  • Run a simple Cluster on Kubernetes
  • Using Kustomize, adapt the Cluster Kubernetes resources to your scenario
This guide assumes you have a running Kubernetes cluster to deploy to and have properly configured `kubectl`.

Prepare Configuration Values

Configuring the Secret Resource

In Kubernetes, Secret objects are used to hold values such as tokens, or private keys.

# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: secret-config
type: Opaque
data:
  cluster-secret: <INSERT_SECRET>

Cluster Secret

To generate the cluster_secret value in secret.yaml, run the following and insert the output in the appropriate place in the secret.yaml file:

$ od  -vN 32 -An -tx1 /dev/urandom | tr -d ' \n' | base64 -w 0 -

Bootstrap Peer ID and Private Key

To generate the values for bootstrap_peer_id and bootstrap_peer_priv_key, install ipfs-key and then run the following:

$ ipfs-key | base64 -w 0

Copy the id into the env-configmap.yaml file. I.e:

# env-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: env-config
data:
  bootstrap-peer-id: <INSERT_PEER_ID>

Then copy the private key value and run the following with it:

$ echo "<INSERT_PRIV_KEY_VALUE_HERE>" | base64 -w 0 -

Copy the output to the secret.yaml file.

# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: secret-config
type: Opaque
data:
  cluster-secret: <INSERT_SECRET>
  bootstrap-peer-priv-key: <INSERT_KEY>

Defining a StatefulSet

From the Kubernetes documentation on StatefulSets:

Manages the deployment and scaling of a set of Pods, and provides guarantees about the ordering and uniqueness of these Pods.

Like a Deployment, a StatefulSet manages Pods that are based on an identical container spec. Unlike a Deployment, a StatefulSet maintains a sticky identity for each of their Pods. These pods are created from the same spec, but are not interchangeable: each has a persistent identifier that it maintains across any rescheduling.

This means for us, that any Kubernetes generated configuration, such as hostnames, i.e. ipfs-cluster-0, will be associated with the same Pod and VolumeClaim, which means for example, hostnames will always be associated with the same peer id that is stored in the ~/.ipfs-cluster/service.json file. And all this is important, because it allows us to bootstrap our ipfs-cluster without a spnning up a specific bootstraping peer.

Breaking the StatefulSet definition into chunks, the first is the preamble and start of the StatefulSet spec:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: ipfs-cluster
spec:
  serviceName: ipfs-cluster
  replicas: 3
  selector:
    matchLabels:
      app: ipfs-cluster

Following that is the definition of the go-ipfs container:

  template:
    metadata:
      labels:
        app: ipfs-cluster
    spec:
      initContainers:
        - name: configure-ipfs
          image: "ipfs/go-ipfs:v0.4.18"
          command: ["sh", "/custom/configure-ipfs.sh"]
          volumeMounts:
            - name: ipfs-storage
              mountPath: /data/ipfs
            - name: configure-script
              mountPath: /custom
      containers:
        - name: ipfs
          image: "ipfs/go-ipfs:v0.4.18"
          imagePullPolicy: IfNotPresent
          env:
            - name: IPFS_FD_MAX
              value: "4096"
          ports:
            - name: swarm
              protocol: TCP
              containerPort: 4001
            - name: swarm-udp
              protocol: UDP
              containerPort: 4002
            - name: api
              protocol: TCP
              containerPort: 5001
            - name: ws
              protocol: TCP
              containerPort: 8081
            - name: http
              protocol: TCP
              containerPort: 8080
          livenessProbe:
            tcpSocket:
              port: swarm
            initialDelaySeconds: 30
            timeoutSeconds: 5
            periodSeconds: 15
          volumeMounts:
            - name: ipfs-storage
              mountPath: /data/ipfs
            - name: configure-script
              mountPath: /custom
          resources:
            {}

Take note of the initContainers section, which is used to configure the ipfs node with production appropriate values, see Defining Configuration Scripts.

Next we define the ipfs-cluster container:

        - name: ipfs-cluster
          image: "ipfs/ipfs-cluster:latest"
          imagePullPolicy: IfNotPresent
          command: ["sh", "/custom/entrypoint.sh"]
          envFrom:
            - configMapRef:
                name: env-config
          env:
            - name: BOOTSTRAP_PEER_ID
              valueFrom:
                configMapRef:
                  name: env-config
                  key: bootstrap-peer-id
            - name: BOOTSTRAP_PEER_PRIV_KEY
              valueFrom:
                secretKeyRef:
                  name: secret-config
                  key: bootstrap-peer-priv-key
            - name: CLUSTER_SECRET
              valueFrom:
                secretKeyRef:
                  name: secret-config
                  key: cluster-secret
            - name: CLUSTER_MONITOR_PING_INTERVAL
              value: "3m"
            - name: SVC_NAME
              value: $(CLUSTER_SVC_NAME)
          ports:
            - name: api-http
              containerPort: 9094
              protocol: TCP
            - name: proxy-http
              containerPort: 9095
              protocol: TCP
            - name: cluster-swarm
              containerPort: 9096
              protocol: TCP
          livenessProbe:
            tcpSocket:
              port: cluster-swarm
            initialDelaySeconds: 5
            timeoutSeconds: 5
            periodSeconds: 10
          volumeMounts:
            - name: cluster-storage
              mountPath: /data/ipfs-cluster
            - name: configure-script
              mountPath: /custom
          resources:
            {}

Note that BOOTSTRAP_PEER_ID and BOOTSTRAP_PEER_PRIV_KEY were the values we defined earlier, they will be used only be the very first ipfs-cluster container ipfs-cluster-0 and then BOOTSTRAP_PEER_ID will be used to pass the bootstrapping multiaddress to the other ipfs-cluster containers.

Finally, we define the volumes for the configuration scripts and also the data volumes for the ipfs and ipfs-cluster containers:

      volumes:
      - name: configure-script
        configMap:
          name: ipfs-cluster-set-bootstrap-conf


  volumeClaimTemplates:
    - metadata:
        name: cluster-storage
      spec:
        storageClassName: standard
        accessModes: ["ReadWriteOnce"]
        persistentVolumeReclaimPolicy: Retain
        resources:
          requests:
            storage: 5Gi
    - metadata:
        name: ipfs-storage
      spec:
        storageClassName: standard
        accessModes: ["ReadWriteOnce"]
        persistentVolumeReclaimPolicy: Retain
        resources:
          requests:
            storage: 200Gi

Depending on your cloud provider, you will have to change the value of storageClassName to the appropriate value.

The StatefulSet definition as an entirety, can be found at github.com/lanzafame/ipfs-cluster-k8s.

Defining Configuration Scripts

The ConfigMap contains two scripts, entrypoint.sh which enables hands-free bootstrapping of the ipfs-cluster cluster and configure-ipfs.sh which configures the ipfs daemon with production values. For more information about configuring ipfs for production, see go-ipfs configuration tweaks.

apiVersion: v1
kind: ConfigMap
metadata:
  name: ipfs-cluster-set-bootstrap-conf
data:
  entrypoint.sh: |
    #!/bin/sh
    user=ipfs

    # This is a custom entrypoint for k8s designed to connect to the bootstrap
    # node running in the cluster. It has been set up using a configmap to
    # allow changes on the fly.


    if [ ! -f /data/ipfs-cluster/service.json ]; then
      ipfs-cluster-service init
    fi

    PEER_HOSTNAME=`cat /proc/sys/kernel/hostname`

    grep -q ".*ipfs-cluster-0.*" /proc/sys/kernel/hostname
    if [ $? -eq 0 ]; then
      CLUSTER_ID=${BOOTSTRAP_PEER_ID} \
      CLUSTER_PRIVATEKEY=${BOOTSTRAP_PEER_PRIV_KEY} \
      exec ipfs-cluster-service daemon --upgrade
    else
      BOOTSTRAP_ADDR=/dns4/${SVC_NAME}-0/tcp/9096/ipfs/${BOOTSTRAP_PEER_ID}

      if [ -z $BOOTSTRAP_ADDR ]; then
        exit 1
      fi
      # Only ipfs user can get here
      exec ipfs-cluster-service daemon --upgrade --bootstrap $BOOTSTRAP_ADDR --leave
    fi

  configure-ipfs.sh: |
    #!/bin/sh
    set -e
    set -x
    user=ipfs
    # This is a custom entrypoint for k8s designed to run ipfs nodes in an appropriate
    # setup for production scenarios.

    mkdir -p /data/ipfs && chown -R ipfs /data/ipfs

    if [ -f /data/ipfs/config ]; then
      if [ -f /data/ipfs/repo.lock ]; then
        rm /data/ipfs/repo.lock
      fi
      exit 0
    fi

    ipfs init --profile=badgerds,server
    ipfs config --json Addresses.API /ip4/0.0.0.0/tcp/5001
    ipfs config --json Addresses.Gateway /ip4/0.0.0.0/tcp/8080
    ipfs config --json Swarm.ConnMgr.HighWater 2000
    ipfs config --json Datastore.BloomFilterSize 1048576
    ipfs config Datastore.StorageMax 100GB

Exposing IPFS Cluster Endpoints

The final step for us is to define the Service which expose the IPFS Cluster endpoints to the outside the Pod.

apiVersion: v1
kind: Service
metadata:
  name: ipfs-cluster
  annotations:
      external-dns.alpha.kubernetes.io/hostname: change.me.com
  labels:
    app: ipfs-cluster
spec:
  type: LoadBalancer
  ports:
    - name: swarm
      targetPort: swarm
      port: 4001
    - name: swarm-udp
      targetPort: swarm-udp
      port: 4002
    - name: ws
      targetPort: ws
      port: 8081
    - name: http
      targetPort: http
      port: 8080
    - name: api-http
      targetPort: api-http
      port: 9094
    - name: proxy-http
      targetPort: proxy-http
      port: 9095
    - name: cluster-swarm
      targetPort: cluster-swarm
      port: 9096
  selector:
    app: ipfs-cluster

Depending on where and how you have set up your Kubernetes cluster, you may be able to make use of the ExternalDNS annotation external-dns.alpha.kubernetes.io/hostname, which will automatically take the provided value, change.me.com, and create a DNS record in the configured DNS provider, i.e. AWS Route53 or Google CloudDNS.

Also note that the targetPort fields are using the named ports from the PodSpec defined in the StatefulSet resource.