Overview

While writing Tagging Asteroids with MongoDB I was able to quickly provision a cluster on the free tier of MongoDB Atlas. What I originally wanted was to quickly stand up a MongoDB instance in my Kubernetes environment but found most documentation too complex for my usecase. Just give me a MongoDB “dialtone” as quickly as possible without having to install the binaries on my system.

NOTE: This deployment pattern should not be used in production. Strongly recommend a StatefulSet for MongoDB Replica Sets.

Easiest way to standup Kubernetes as a Developer

Install Docker on your machine then install kind

Manifests

You will find all the code for this deployment on my GitHub https://github.com/fullaware/k8s-mongodb

Kustomize

The kustomization.yaml is the entrypoint to deploying MongoDB on Kubernetes. Kustomize allows us to package up all the other manifests (yaml files) and deploy them to a specific namespace (mongodb in this case). Kustomize also allows us to generate secrets so that we don’t have to do the base64 encoding by hand. In a production environment you would use something like Hashicorp Vault to centrally store passwords.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: mongodb
resources:
  - mongodb-namespace.yaml          # Create MongoDB namespace
  - mongodb-dbinit-configmap.yaml   # Initialize DB with data
  - mongodb-pvc.yaml                # Request storage
  - mongodb-svc.yaml                # Expose MongoDB as service
  - mongodb-deployment.yaml         # Deploy container
secretGenerator:                    
- name: mongo-creds
  literals:
  - username=admin                  # MONGO_INITDB_ROOT_USERNAME
  - password=Candy123               # MONGO_INITDB_ROOT_PASSWORD

Initialize MongoDB container with data using a configmap

I prefer this method for it’s simplicity but comes with a major downside. You are limited to 1MB due to the objects size in etcd. If you need to initialize the database with more than 1MB, consider using an Init Container to download a mongodump file then mongorestore the file into the database.

The MongoDB image on Docker Hub will look for all scripts found in its mountpoint at /docker-entrypoint-initdb.d and execute them in alphabetical order.

Create a configmap and populate the data with the contents of a dbinit.js file like so [truncated for easier reading, see mongodb-dbinit-configmap.yaml]:

apiVersion: v1
kind: ConfigMap
metadata:
  name: dbinit-script
data:
  dbinit.js: |-
    db = new Mongo().getDB("asteroids");

    db.createCollection('asteroids', { capped: false });

    db.asteroids.insert([{
    "_id": 1000,
    "name": "Bennu",
    "elements": [
      100,
      101,
      108
    ]
    }])

Persistent Volume Claim

For our usecase we don’t want our data to disappear when we restart the deployment so we want to create a persistent volume claim.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongo-data
spec:
  accessModes:
    - ReadWriteOnce 
  resources:
    requests:
      storage: 1Gi

Service

Here we will expose the MongoDB deployment as a service on port 27017. This will make it accessible to other applications running on the Kubernetes cluster.

apiVersion: v1
kind: Service
metadata:
  labels:
    app: mongodb
  name: mongodb-svc
spec:
  ports:
  - port: 27017
    protocol: TCP
    targetPort: 27017
  selector:
    app: mongodb
  type: ClusterIP 

Deployment

All of our hard work comes together in the deployment. Here we will map our persistent volume for our /data/db as well as our dbinit-script configmap. Default username and password are provided by the secret that Kustomize generated earlier.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: mongodb
  name: mongodb
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mongodb
  strategy: {}
  template:
    metadata:
      labels:
        app: mongodb
    spec:
      containers:
      - image: mongo
        name: mongo
        args: ["--dbpath","/data/db"]
        env:
        - name: MONGO_INITDB_ROOT_USERNAME
          valueFrom:
            secretKeyRef:
              name: mongo-creds
              key: username
        - name: MONGO_INITDB_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mongo-creds
              key: password
        volumeMounts:
        - name: "mongo-data-dir"
          mountPath: "/data/db"
        - name: dbinit-script
          mountPath: /docker-entrypoint-initdb.d 
      volumes:
      - name: "mongo-data-dir"
        persistentVolumeClaim:
          claimName: "mongo-data"
      - name: dbinit-script
        configMap:
          name: dbinit-script

Running MongoDB on Kubernetes

git clone https://github.com/fullaware/k8s-mongodb.git
cd k8s-mongodb

This next part is important, notice we will use a -k instead of -f which instructs kubectl to run the kustomization.yaml instead of just running all the yaml files in the folder.

kubectl apply -k .

Verify MongoDB server

First get shell access into the deployment.

kubectl exec deployment/mongodb -n mongodb -it -- /bin/bash

Then auth into the server

mongosh -u admin -p Candy123

Exit mongosh then exit from the deployment because now we want to access the MongoDB instance using either our programming language of choice, MongoDB Compass or mongosh from our local machine.

Access deployment on localhost

Use kubectl to forward the port 27017 from the mongodb-svc to localhost

kubectl port-forward --address 0.0.0.0 svc/mongodb-svc 27017:27017 -n mongodb

From there we should be able to use mongosh client to connect to the MongoDB instance by connecting to localhost

mongosh -u admin -p Candy123

Once we are in the Mongo Shell, let’s look for databases

test> show dbs
admin      100.00 KiB
asteroids   80.00 KiB
config      72.00 KiB
local       72.00 KiB

Now use the asteroids database

test> use asteroids
switched to db asteroids

Next we will look at the collections in this database

asteroids> show collections
asteroids
elements

Find all the documents in the asteroids collection

asteroids> db.asteroids.find()
[
  { _id: 1000, name: 'Bennu', elements: [ 100, 101, 108 ] },
  { _id: 1001, name: 'Ceres', elements: [ 106, 103, 108 ] },
  { _id: 1002, name: 'Pallas', elements: [ 103, 102, 105 ] },
  { _id: 1003, name: 'Juno', elements: [ 107, 106, 100 ] },
  { _id: 1004, name: 'Vesta', elements: [ 108, 101, 103 ] },
  { _id: 1005, name: 'Astraea', elements: [ 105, 101, 106 ] }
]